Unverified Commit 80e3964d authored by Toni-SM's avatar Toni-SM Committed by GitHub

Adds new external project / internal task template generator (#2039)

# Description

This PR adds the implementation to generate Isaac Lab's external project
or internal task.

Just run `./isaaclab.sh --new` or `isaaclab.bat --new` (or `-n`) and let
yourself go!

## 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`
- [ ] 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

<!--
As you go through the checklist above, you can mark something as done by
putting an x character in it

For example,
- [x] I have done this task
- [ ] I have not done this task
-->

---------
Signed-off-by: 's avatarToni-SM <toni.semu@gmail.com>
Signed-off-by: 's avatarKelly Guo <kellyguo123@hotmail.com>
Co-authored-by: 's avatarKelly Guo <kellyg@nvidia.com>
parent f774425b
Building your Own Project
=========================
.. _template-generator:
Build your Own Project or Task
==============================
Traditionally, building new projects that utilize Isaac Lab's features required creating your own
extensions within the Isaac Lab repository. However, this approach can obscure project visibility and
complicate updates from one version of Isaac Lab to another. To circumvent these challenges, we now
provide a pre-configured and customizable `extension template <https://github.com/isaac-sim/IsaacLabExtensionTemplate>`_
for creating projects in an isolated environment.
complicate updates from one version of Isaac Lab to another. To circumvent these challenges,
we now provide a command-line tool (**template generator**) for creating Isaac Lab-based projects and tasks.
This template serves three distinct use cases:
The template generator enables you to create an:
* **Project Template**: Provides essential access to Isaac Sim and Isaac Lab's features, making it ideal for projects
that require a standalone environment.
* **Python Package**: Facilitates integration with Isaac Sim's native or virtual Python environment, allowing for
the creation of Python packages that can be shared and reused across multiple projects.
* **Omniverse Extension**: Supports direct integration into Omniverse extension workflow.
* **External project** (recommended): An isolated project that is not part of the Isaac Lab repository. This approach
works outside of the core Isaac Lab repository, ensuring that your development efforts remain self-contained. Also,
it allows your code to be run as an extension in Omniverse.
.. note::
.. hint::
For the external project, the template generator will initialize a new Git repository in the specified directory.
You can push the generated content to your own remote repository (e.g. GitHub) and share it with others.
* **Internal task**: A task that is part of the Isaac Lab repository. This approach should only be used to create
new tasks within the Isaac Lab repository in order to contribute to it.
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.
Then, run the following command to generate a new external project or internal task:
.. tab-set::
:sync-group: os
.. tab-item:: :icon:`fa-brands fa-linux` Linux
:sync: linux
We recommend using the extension template for new projects, as it provides a more streamlined and
efficient workflow. Additionally it ensures that your project remains up-to-date with the latest
features and improvements in Isaac Lab.
.. code-block:: bash
./isaaclab.sh --new # or "./isaaclab.sh -n"
Installation
------------
.. tab-item:: :icon:`fa-brands fa-windows` Windows
:sync: windows
Install Isaac Lab by following the `installation guide <../../setup/installation/index.html>`_. We recommend using the conda installation as it simplifies calling Python scripts from the terminal.
.. code-block:: batch
isaaclab.bat --new :: or "isaaclab.bat -n"
The generator will guide you in setting up the project/task for your needs by asking you the following questions:
* Type of project/task (external or internal), and project/task path or names according to the selected type.
* Isaac Lab workflows (see :ref:`feature-workflows`).
* Reinforcement learning libraries (see :ref:`rl-frameworks`), and algorithms (if the selected libraries support multiple algorithms).
External project usage (once generated)
---------------------------------------
Once the external project is generated, a ``README.md`` file will be created in the specified directory.
This file will contain instructions on how to install the project and run the tasks.
Here are some general commands to get started with it:
.. note::
Clone the extension template repository separately from the Isaac Lab installation (i.e. outside the IsaacLab directory):
If Isaac Lab is not installed in a conda environment or in a (virtual) Python environment, use ``FULL_PATH_TO_ISAACLAB/isaaclab.sh -p``
(or ``FULL_PATH_TO_ISAACLAB\isaaclab.bat -p`` on Windows) instead of ``python`` to run the commands below.
.. code:: bash
* Install the project (in editable mode).
# Option 1: HTTPS
git clone https://github.com/isaac-sim/IsaacLabExtensionTemplate.git
.. code:: bash
# Option 2: SSH
git clone git@github.com:isaac-sim/IsaacLabExtensionTemplate.git
python -m pip install -e source/<given-project-name>
Throughout the repository, the name ``ext_template`` only serves as an example and we provide a script to rename all the references to it automatically:
* List the tasks available in the project.
.. code:: bash
.. warning::
# Enter the repository
cd IsaacLabExtensionTemplate
If the task names change, it may be necessary to update the search pattern ``"Template-"``
(in the ``scripts/list_envs.py`` file) so that they can be listed.
# Rename all occurrences of ext_template (in files/directories) to your_fancy_extension_name
python scripts/rename_template.py your_fancy_extension_name
.. code:: bash
Using a python interpreter that has Isaac Lab installed, install the library:
python scripts/list_envs.py
.. code:: bash
* Run a task.
python -m pip install -e source/ext_template
.. code:: bash
python scripts/<specific-rl-library>/train.py --task=<Task-Name>
For more details, please follow the instructions in the `extension template repository <https://github.com/isaac-sim/IsaacLabExtensionTemplate>`_.
For more details, please follow the instructions in the generated project's ``README.md`` file.
.. _rl-frameworks:
Reinforcement Learning Library Comparison
=========================================
......
......@@ -203,7 +203,7 @@ Clone the Isaac Lab repository into your workspace:
./isaaclab.sh --help
usage: isaaclab.sh [-h] [-i] [-f] [-p] [-s] [-t] [-o] [-v] [-d] [-c] -- Utility to manage Isaac Lab.
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.
......@@ -215,6 +215,7 @@ Clone the Isaac Lab repository into your workspace:
-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'.
.. tab-item:: :icon:`fa-brands fa-windows` Windows
......@@ -224,7 +225,7 @@ Clone the Isaac Lab repository into your workspace:
isaaclab.bat --help
usage: isaaclab.bat [-h] [-i] [-f] [-p] [-s] [-v] [-d] [-c] -- Utility to manage Isaac Lab.
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.
......@@ -235,6 +236,7 @@ Clone the Isaac Lab repository into your workspace:
-t, --test Run all python unittest 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'.
......
......@@ -7,7 +7,7 @@ that are built on top of Isaac Lab. Isaac Lab pip packages **do not** include an
training, inferencing, or running standalone workflows such as demos and examples. Therefore, users are required
to define your own runner scripts when installing Isaac Lab from pip.
To learn about how to set up your own extension project on top of Isaac Lab, visit `Extension Template <../../overview/developer-guide/template.html>`_.
To learn about how to set up your own project on top of Isaac Lab, see :ref:`template-generator`.
.. note::
......
......@@ -211,7 +211,7 @@ Clone the Isaac Lab repository into your workspace:
./isaaclab.sh --help
usage: isaaclab.sh [-h] [-i] [-f] [-p] [-s] [-t] [-o] [-v] [-d] [-c] -- Utility to manage Isaac Lab.
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.
......@@ -223,6 +223,7 @@ Clone the Isaac Lab repository into your workspace:
-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'.
.. tab-item:: :icon:`fa-brands fa-windows` Windows
......@@ -232,7 +233,7 @@ Clone the Isaac Lab repository into your workspace:
isaaclab.bat --help
usage: isaaclab.bat [-h] [-i] [-f] [-p] [-s] [-v] [-d] [-c] -- Utility to manage Isaac Lab.
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.
......@@ -243,6 +244,7 @@ Clone the Isaac Lab repository into your workspace:
-t, --test Run all python unittest 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'.
Installation
......
......@@ -236,7 +236,7 @@ goto :eof
rem Print the usage description
:print_help
echo.
echo usage: %~nx0 [-h] [-i] [-f] [-p] [-s] [-v] [-d] [-c] -- Utility to manage extensions in Isaac Lab.
echo usage: %~nx0 [-h] [-i] [-f] [-p] [-s] [-v] [-d] [-n] [-c] -- Utility to manage extensions in Isaac Lab.
echo.
echo optional arguments:
echo -h, --help Display the help content.
......@@ -247,6 +247,7 @@ echo -s, --sim Run the simulator executable (isaac-sim.bat) provi
echo -t, --test Run all python unittest tests.
echo -v, --vscode Generate the VSCode settings file from template.
echo -d, --docs Build the documentation from source using sphinx.
echo -n, --new Create a new external project or internal task from template.
echo -c, --conda [NAME] Create the conda environment for Isaac Lab. Default name is 'env_isaaclab'.
echo.
goto :eof
......@@ -463,6 +464,44 @@ if "%arg%"=="-i" (
)
!isaacsim_exe! --ext-folder %ISAACLAB_PATH%\source !allArgs1
goto :end
) else if "%arg%"=="-n" (
rem run the template generator script
call :extract_python_exe
set "allArgs="
for %%a in (%*) do (
REM Append each argument to the variable, skip the first one
if defined skip (
set "allArgs=!allArgs! %%a"
) else (
set "skip=1"
)
)
echo [INFO] Installing template dependencies...
!python_exe! -m pip install -q -r tools\template\requirements.txt
echo.
echo [INFO] Running template generator...
echo.
!python_exe! tools\template\cli.py !allArgs!
goto :end
) else if "%arg%"=="--new" (
rem run the template generator script
call :extract_python_exe
set "allArgs="
for %%a in (%*) do (
REM Append each argument to the variable, skip the first one
if defined skip (
set "allArgs=!allArgs! %%a"
) else (
set "skip=1"
)
)
echo [INFO] Installing template dependencies...
!python_exe! -m pip install -q -r tools\template\requirements.txt
echo.
echo [INFO] Running template generator...
echo.
!python_exe! tools\template\cli.py !allArgs!
goto :end
) else if "%arg%"=="-t" (
rem run the python provided by Isaac Sim
call :extract_python_exe
......
......@@ -241,7 +241,7 @@ update_vscode_settings() {
# print the usage description
print_help () {
echo -e "\nusage: $(basename "$0") [-h] [-i] [-f] [-p] [-s] [-t] [-o] [-v] [-d] [-c] -- Utility to manage Isaac Lab."
echo -e "\nusage: $(basename "$0") [-h] [-i] [-f] [-p] [-s] [-t] [-o] [-v] [-d] [-n] [-c] -- Utility to manage Isaac Lab."
echo -e "\noptional arguments:"
echo -e "\t-h, --help Display the help content."
echo -e "\t-i, --install [LIB] Install the extensions inside Isaac Lab and learning frameworks as extra dependencies. Default is 'all'."
......@@ -252,6 +252,7 @@ print_help () {
echo -e "\t-o, --docker Run the docker container helper script (docker/container.sh)."
echo -e "\t-v, --vscode Generate the VSCode settings file from template."
echo -e "\t-d, --docs Build the documentation from source using sphinx."
echo -e "\t-n, --new Create a new external project or internal task from template."
echo -e "\t-c, --conda [NAME] Create the conda environment for Isaac Lab. Default name is 'env_isaaclab'."
echo -e "\n" >&2
}
......@@ -375,6 +376,17 @@ while [[ $# -gt 0 ]]; do
# exit neatly
break
;;
-n|--new)
# run the template generator script
python_exe=$(extract_python_exe)
shift # past argument
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 $@
# exit neatly
break
;;
-t|--test)
# run the python provided by isaacsim
python_exe=$(extract_python_exe)
......
......@@ -67,6 +67,8 @@ from isaaclab_rl.rl_games import RlGamesGpuEnv, RlGamesVecEnvWrapper
import isaaclab_tasks # noqa: F401
from isaaclab_tasks.utils import get_checkpoint_path, load_cfg_from_registry, parse_env_cfg
# PLACEHOLDER: Extension template (do not remove this comment)
def main():
"""Play with RL-Games agent."""
......
......@@ -70,6 +70,8 @@ from isaaclab_rl.rl_games import RlGamesGpuEnv, RlGamesVecEnvWrapper
import isaaclab_tasks # noqa: F401
from isaaclab_tasks.utils.hydra import hydra_task_config
# PLACEHOLDER: Extension template (do not remove this comment)
@hydra_task_config(args_cli.task, "rl_games_cfg_entry_point")
def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agent_cfg: dict):
......
......@@ -61,6 +61,8 @@ from isaaclab_rl.rsl_rl import RslRlOnPolicyRunnerCfg, RslRlVecEnvWrapper, expor
import isaaclab_tasks # noqa: F401
from isaaclab_tasks.utils import get_checkpoint_path, parse_env_cfg
# PLACEHOLDER: Extension template (do not remove this comment)
def main():
"""Play with RSL-RL agent."""
......
......@@ -67,6 +67,8 @@ import isaaclab_tasks # noqa: F401
from isaaclab_tasks.utils import get_checkpoint_path
from isaaclab_tasks.utils.hydra import hydra_task_config
# PLACEHOLDER: Extension template (do not remove this comment)
torch.backends.cuda.matmul.allow_tf32 = True
torch.backends.cudnn.allow_tf32 = True
torch.backends.cudnn.deterministic = False
......
......@@ -64,6 +64,8 @@ from isaaclab_rl.sb3 import Sb3VecEnvWrapper, process_sb3_cfg
import isaaclab_tasks # noqa: F401
from isaaclab_tasks.utils.parse_cfg import get_checkpoint_path, load_cfg_from_registry, parse_env_cfg
# PLACEHOLDER: Extension template (do not remove this comment)
def main():
"""Play with stable-baselines agent."""
......
......@@ -69,6 +69,8 @@ from isaaclab_rl.sb3 import Sb3VecEnvWrapper, process_sb3_cfg
import isaaclab_tasks # noqa: F401
from isaaclab_tasks.utils.hydra import hydra_task_config
# PLACEHOLDER: Extension template (do not remove this comment)
@hydra_task_config(args_cli.task, "sb3_cfg_entry_point")
def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agent_cfg: dict):
......
......@@ -91,6 +91,8 @@ from isaaclab_rl.skrl import SkrlVecEnvWrapper
import isaaclab_tasks # noqa: F401
from isaaclab_tasks.utils import get_checkpoint_path, load_cfg_from_registry, parse_env_cfg
# PLACEHOLDER: Extension template (do not remove this comment)
# config shortcuts
algorithm = args_cli.algorithm.lower()
......
......@@ -100,6 +100,8 @@ from isaaclab_rl.skrl import SkrlVecEnvWrapper
import isaaclab_tasks # noqa: F401
from isaaclab_tasks.utils.hydra import hydra_task_config
# PLACEHOLDER: Extension template (do not remove this comment)
# config shortcuts
algorithm = args_cli.algorithm.lower()
agent_cfg_entry_point = "skrl_cfg_entry_point" if algorithm in ["ppo"] else f"skrl_{algorithm}_cfg_entry_point"
......
# Copyright (c) 2022-2025, The Isaac Lab Project Developers.
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
# Copyright (c) 2022-2025, The Isaac Lab Project Developers.
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
import enum
import glob
import os
from collections.abc import Callable
import rich.console
import rich.table
from common import ROOT_DIR, TEMPLATE_DIR
from generator import generate
from InquirerPy import inquirer, separator
class CLIHandler:
"""CLI handler for the Isaac Lab template."""
def __init__(self):
self.console = rich.console.Console()
@staticmethod
def get_choices(choices: list[str], default: list[str]) -> list[str]:
return default if "all" in choices or "both" in choices else choices
def output_table(self, table: rich.table.Table, new_line_start: bool = True) -> None:
"""Print a rich table to the console.
Args:
table: The table to print.
new_line_start: Whether to print a new line before the table.
"""
self.console.print(table, new_line_start=new_line_start)
def input_select(
self, message: str, choices: list[str], default: str | None = None, long_instruction: str = ""
) -> str:
"""Prompt the user to select an option from a list of choices.
Args:
message: The message to display to the user.
choices: The list of choices to display to the user.
default: The default choice.
long_instruction: The long instruction to display to the user.
Returns:
str: The selected choice.
"""
return inquirer.select(
message=message,
choices=choices,
cycle=True,
default=default,
style=None,
wrap_lines=True,
long_instruction=long_instruction,
).execute()
def input_checkbox(self, message: str, choices: list[str], default: str | None = None) -> list[str]:
"""Prompt the user to select one or more options from a list of choices.
Args:
message: The message to display to the user.
choices: The list of choices to display to the user.
default: The default choice.
Returns:
The selected choices.
"""
def transformer(result: list[str]) -> str:
if "all" in result or "both" in result:
token = "all" if "all" in result else "both"
return f'{token} ({", ".join(choices[:choices.index("---")])})'
return ", ".join(result)
return inquirer.checkbox(
message=message,
choices=[separator.Separator() if "---" in item else item for item in choices],
cycle=True,
default=default,
style=None,
wrap_lines=True,
validate=lambda result: len(result) >= 1,
invalid_message="No option selected (SPACE: select/deselect an option, ENTER: confirm selection)",
transformer=transformer,
).execute()
def input_path(
self,
message: str,
default: str | None = None,
validate: Callable[[str], bool] | None = None,
invalid_message: str = "",
) -> str:
"""Prompt the user to input a path.
Args:
message: The message to display to the user.
default: The default path.
validate: A callable to validate the path.
invalid_message: The message to display to the user if the path is invalid.
Returns:
The input path.
"""
return inquirer.filepath(
message=message,
default=default if default is not None else "",
validate=validate,
invalid_message=invalid_message,
).execute()
def input_text(
self,
message: str,
default: str | None = None,
validate: Callable[[str], bool] | None = None,
invalid_message: str = "",
) -> str:
"""Prompt the user to input a text.
Args:
message: The message to display to the user.
default: The default text.
validate: A callable to validate the text.
invalid_message: The message to display to the user if the text is invalid.
Returns:
The input text.
"""
return inquirer.text(
message=message,
default=default if default is not None else "",
validate=validate,
invalid_message=invalid_message,
).execute()
class State(str, enum.Enum):
Yes = "[green]yes[/green]"
No = "[red]no[/red]"
def _get_algorithms_per_rl_library():
data = {"rl_games": [], "rsl_rl": [], "skrl": [], "sb3": []}
for file in glob.glob(os.path.join(TEMPLATE_DIR, "agents", "*_cfg")):
for rl_library in data.keys():
basename = os.path.basename(file).replace("_cfg", "")
if basename.startswith(f"{rl_library}_"):
data[rl_library].append(basename.replace(f"{rl_library}_", "").upper())
return data
def main() -> None:
"""Main function to run template generation from CLI."""
cli_handler = CLIHandler()
# project type
is_external_project = (
cli_handler.input_select(
"Task type:",
choices=["External", "Internal"],
long_instruction=(
"External (recommended): task/project is in its own folder/repo outside the Isaac Lab project.\n"
"Internal: the task is implemented within the Isaac Lab project (in source/isaaclab_tasks)."
),
).lower()
== "external"
)
# project path (if 'external')
project_path = None
if is_external_project:
project_path = cli_handler.input_path(
"Project path:",
default=os.path.dirname(ROOT_DIR) + os.sep,
validate=lambda path: not os.path.abspath(path).startswith(os.path.abspath(ROOT_DIR)),
invalid_message="External project path cannot be within the Isaac Lab project",
)
# project/task name
project_name = cli_handler.input_text(
"Project name:" if is_external_project else "Task's folder name:",
validate=lambda name: name.isidentifier(),
invalid_message=(
"Project/task name must be a valid identifier (Letters, numbers and underscores only. No spaces, etc.)"
),
)
# Isaac Lab workflow
# - show supported workflows and features
workflow_table = rich.table.Table(title="RL environment features support according to Isaac Lab workflows")
workflow_table.add_column("Environment feature", no_wrap=True)
workflow_table.add_column("Direct", justify="center")
workflow_table.add_column("Manager-based", justify="center")
workflow_table.add_row("Single-agent", State.Yes, State.Yes)
workflow_table.add_row("Multi-agent", State.Yes, State.No)
workflow_table.add_row("Fundamental/composite spaces (apart from 'Box')", State.Yes, State.No)
cli_handler.output_table(workflow_table)
# - prompt for workflows
supported_workflows = ["Direct | single-agent", "Direct | multi-agent", "Manager-based | single-agent"]
workflow = cli_handler.get_choices(
cli_handler.input_checkbox("Isaac Lab workflow:", choices=[*supported_workflows, "---", "all"]),
default=supported_workflows,
)
workflow = [{"name": item.split(" | ")[0].lower(), "type": item.split(" | ")[1].lower()} for item in workflow]
# RL library
rl_library_algorithms = []
algorithms_per_rl_library = _get_algorithms_per_rl_library()
# - show supported RL libraries and features
rl_library_table = rich.table.Table(title="Supported RL libraries")
rl_library_table.add_column("RL/training feature", no_wrap=True)
rl_library_table.add_column("rl_games")
rl_library_table.add_column("rsl_rl")
rl_library_table.add_column("skrl")
rl_library_table.add_column("sb3")
rl_library_table.add_row("ML frameworks", "PyTorch", "PyTorch", "PyTorch, JAX", "PyTorch")
rl_library_table.add_row(
"Algorithms",
", ".join(algorithms_per_rl_library.get("rl_games", [])),
", ".join(algorithms_per_rl_library.get("rsl_rl", [])),
", ".join(algorithms_per_rl_library.get("skrl", [])),
", ".join(algorithms_per_rl_library.get("sb3", [])),
)
rl_library_table.add_row("Relative performance", "~1X", "~1X", "~1X", "~0.03X")
rl_library_table.add_row("Distributed training", State.Yes, State.No, State.Yes, State.No)
rl_library_table.add_row("Vectorized training", State.Yes, State.Yes, State.Yes, State.No)
rl_library_table.add_row("Fundamental/composite spaces", State.No, State.No, State.Yes, State.No)
cli_handler.output_table(rl_library_table)
# - prompt for RL libraries
supported_rl_libraries = ["rl_games", "rsl_rl", "skrl", "sb3"]
selected_rl_libraries = cli_handler.get_choices(
cli_handler.input_checkbox("RL library:", choices=[*supported_rl_libraries, "---", "all"]),
default=supported_rl_libraries,
)
# - prompt for algorithms per RL library
for rl_library in selected_rl_libraries:
algorithms = algorithms_per_rl_library.get(rl_library, [])
if len(algorithms) > 1:
algorithms = cli_handler.get_choices(
cli_handler.input_checkbox(f"RL algorithms for {rl_library}:", choices=[*algorithms, "---", "all"]),
default=algorithms,
)
rl_library_algorithms.append({"name": rl_library, "algorithms": [item.lower() for item in algorithms]})
specification = {
"external": is_external_project,
"path": project_path,
"name": project_name,
"workflows": workflow,
"rl_libraries": rl_library_algorithms,
}
generate(specification)
if __name__ == "__main__":
main()
# Copyright (c) 2022-2025, The Isaac Lab Project Developers.
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
import os
ROOT_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
TASKS_DIR = os.path.join(ROOT_DIR, "source", "isaaclab_tasks", "isaaclab_tasks")
TEMPLATE_DIR = os.path.join(ROOT_DIR, "tools", "template", "templates")
This diff is collapsed.
# CLI management
InquirerPy
rich
# templating
Jinja2
params:
seed: 42
# environment wrapper clipping
env:
# added to the wrapper
clip_observations: 5.0
# can make custom wrapper?
clip_actions: 1.0
algo:
name: a2c_continuous
model:
name: continuous_a2c_logstd
# doesn't have this fine grained control but made it close
network:
name: actor_critic
separate: False
space:
continuous:
mu_activation: None
sigma_activation: None
mu_init:
name: default
sigma_init:
name: const_initializer
val: 0
fixed_sigma: True
mlp:
units: [32, 32]
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: cartpole_direct
env_name: rlgpu
device: 'cuda:0'
device_name: 'cuda:0'
multi_gpu: False
ppo: True
mixed_precision: False
normalize_input: True
normalize_value: True
num_actors: -1 # configured from the script (based on num_envs)
reward_shaper:
scale_value: 0.1
normalize_advantage: True
gamma: 0.99
tau : 0.95
learning_rate: 5e-4
lr_schedule: adaptive
kl_threshold: 0.008
score_to_win: 20000
max_epochs: 150
save_best_after: 50
save_frequency: 25
grad_norm: 1.0
entropy_coef: 0.0
truncate_grads: True
e_clip: 0.2
horizon_length: 32
minibatch_size: 16384
mini_epochs: 8
critic_coef: 4
clip_value: True
seq_length: 4
bounds_loss_coef: 0.0001
# Copyright (c) 2022-2025, The Isaac Lab Project Developers.
# 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 CartpolePPORunnerCfg(RslRlOnPolicyRunnerCfg):
num_steps_per_env = 16
max_iterations = 150
save_interval = 50
experiment_name = "cartpole_direct"
empirical_normalization = False
policy = RslRlPpoActorCriticCfg(
init_noise_std=1.0,
actor_hidden_dims=[32, 32],
critic_hidden_dims=[32, 32],
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,
)
# Reference: https://github.com/DLR-RM/rl-baselines3-zoo/blob/master/hyperparams/ppo.yml#L32
seed: 42
n_timesteps: !!float 1e6
policy: 'MlpPolicy'
n_steps: 16
batch_size: 4096
gae_lambda: 0.95
gamma: 0.99
n_epochs: 20
ent_coef: 0.01
learning_rate: !!float 3e-4
clip_range: !!float 0.2
policy_kwargs: "dict(
activation_fn=nn.ELU,
net_arch=[32, 32],
squash_output=False,
)"
vf_coef: 1.0
max_grad_norm: 1.0
device: "cuda:0"
seed: 42
# Models are instantiated using skrl's model instantiator utility
# https://skrl.readthedocs.io/en/latest/api/utils/model_instantiators.html
models:
separate: True
policy: # see gaussian_model parameters
class: GaussianMixin
clip_actions: False
clip_log_std: True
min_log_std: -20.0
max_log_std: 2.0
initial_log_std: -2.9
fixed_log_std: True
network:
- name: net
input: STATES
layers: [1024, 512]
activations: relu
output: ACTIONS
value: # see deterministic_model parameters
class: DeterministicMixin
clip_actions: False
network:
- name: net
input: STATES
layers: [1024, 512]
activations: relu
output: ONE
discriminator: # see deterministic_model parameters
class: DeterministicMixin
clip_actions: False
network:
- name: net
input: STATES
layers: [1024, 512]
activations: relu
output: ONE
# Rollout memory
# https://skrl.readthedocs.io/en/latest/api/memories/random.html
memory:
class: RandomMemory
memory_size: -1 # automatically determined (same as agent:rollouts)
# AMP memory (reference motion dataset)
# https://skrl.readthedocs.io/en/latest/api/memories/random.html
motion_dataset:
class: RandomMemory
memory_size: 200000
# AMP memory (preventing discriminator overfitting)
# https://skrl.readthedocs.io/en/latest/api/memories/random.html
reply_buffer:
class: RandomMemory
memory_size: 1000000
# AMP agent configuration (field names are from AMP_DEFAULT_CONFIG)
# https://skrl.readthedocs.io/en/latest/api/agents/amp.html
agent:
class: AMP
rollouts: 16
learning_epochs: 6
mini_batches: 2
discount_factor: 0.99
lambda: 0.95
learning_rate: 5.0e-05
learning_rate_scheduler: null
learning_rate_scheduler_kwargs: null
state_preprocessor: RunningStandardScaler
state_preprocessor_kwargs: null
value_preprocessor: RunningStandardScaler
value_preprocessor_kwargs: null
amp_state_preprocessor: RunningStandardScaler
amp_state_preprocessor_kwargs: null
random_timesteps: 0
learning_starts: 0
grad_norm_clip: 0.0
ratio_clip: 0.2
value_clip: 0.2
clip_predicted_values: True
entropy_loss_scale: 0.0
value_loss_scale: 2.5
discriminator_loss_scale: 5.0
amp_batch_size: 512
task_reward_weight: 0.0
style_reward_weight: 1.0
discriminator_batch_size: 4096
discriminator_reward_scale: 2.0
discriminator_logit_regularization_scale: 0.05
discriminator_gradient_penalty_scale: 5.0
discriminator_weight_decay_scale: 1.0e-04
# rewards_shaper_scale: 1.0
time_limit_bootstrap: False
# logging and checkpoint
experiment:
directory: "humanoid_amp_run"
experiment_name: ""
write_interval: auto
checkpoint_interval: auto
# Sequential trainer
# https://skrl.readthedocs.io/en/latest/api/trainers/sequential.html
trainer:
class: SequentialTrainer
timesteps: 80000
environment_info: log
seed: 42
# Models are instantiated using skrl's model instantiator utility
# https://skrl.readthedocs.io/en/latest/api/utils/model_instantiators.html
models:
separate: False
policy: # see gaussian_model parameters
class: GaussianMixin
clip_actions: False
clip_log_std: True
min_log_std: -20.0
max_log_std: 2.0
initial_log_std: 0.0
network:
- name: net
input: STATES
layers: [32, 32]
activations: elu
output: ACTIONS
value: # see deterministic_model parameters
class: DeterministicMixin
clip_actions: False
network:
- name: net
input: STATES
layers: [32, 32]
activations: elu
output: ONE
# Rollout memory
# https://skrl.readthedocs.io/en/latest/api/memories/random.html
memory:
class: RandomMemory
memory_size: -1 # automatically determined (same as agent:rollouts)
# IPPO agent configuration (field names are from IPPO_DEFAULT_CONFIG)
# https://skrl.readthedocs.io/en/latest/api/multi_agents/ippo.html
agent:
class: IPPO
rollouts: 16
learning_epochs: 8
mini_batches: 1
discount_factor: 0.99
lambda: 0.95
learning_rate: 3.0e-04
learning_rate_scheduler: KLAdaptiveLR
learning_rate_scheduler_kwargs:
kl_threshold: 0.008
state_preprocessor: RunningStandardScaler
state_preprocessor_kwargs: null
value_preprocessor: RunningStandardScaler
value_preprocessor_kwargs: null
random_timesteps: 0
learning_starts: 0
grad_norm_clip: 1.0
ratio_clip: 0.2
value_clip: 0.2
clip_predicted_values: True
entropy_loss_scale: 0.0
value_loss_scale: 2.0
kl_threshold: 0.0
rewards_shaper_scale: 1.0
time_limit_bootstrap: False
# logging and checkpoint
experiment:
directory: "cart_double_pendulum_direct"
experiment_name: ""
write_interval: auto
checkpoint_interval: auto
# Sequential trainer
# https://skrl.readthedocs.io/en/latest/api/trainers/sequential.html
trainer:
class: SequentialTrainer
timesteps: 4800
environment_info: log
seed: 42
# Models are instantiated using skrl's model instantiator utility
# https://skrl.readthedocs.io/en/latest/api/utils/model_instantiators.html
models:
separate: True
policy: # see gaussian_model parameters
class: GaussianMixin
clip_actions: False
clip_log_std: True
min_log_std: -20.0
max_log_std: 2.0
initial_log_std: 0.0
network:
- name: net
input: STATES
layers: [32, 32]
activations: elu
output: ACTIONS
value: # see deterministic_model parameters
class: DeterministicMixin
clip_actions: False
network:
- name: net
input: STATES
layers: [32, 32]
activations: elu
output: ONE
# Rollout memory
# https://skrl.readthedocs.io/en/latest/api/memories/random.html
memory:
class: RandomMemory
memory_size: -1 # automatically determined (same as agent:rollouts)
# MAPPO agent configuration (field names are from MAPPO_DEFAULT_CONFIG)
# https://skrl.readthedocs.io/en/latest/api/multi_agents/mappo.html
agent:
class: MAPPO
rollouts: 16
learning_epochs: 8
mini_batches: 1
discount_factor: 0.99
lambda: 0.95
learning_rate: 3.0e-04
learning_rate_scheduler: KLAdaptiveLR
learning_rate_scheduler_kwargs:
kl_threshold: 0.008
state_preprocessor: RunningStandardScaler
state_preprocessor_kwargs: null
shared_state_preprocessor: RunningStandardScaler
shared_state_preprocessor_kwargs: null
value_preprocessor: RunningStandardScaler
value_preprocessor_kwargs: null
random_timesteps: 0
learning_starts: 0
grad_norm_clip: 1.0
ratio_clip: 0.2
value_clip: 0.2
clip_predicted_values: True
entropy_loss_scale: 0.0
value_loss_scale: 2.0
kl_threshold: 0.0
rewards_shaper_scale: 1.0
time_limit_bootstrap: False
# logging and checkpoint
experiment:
directory: "cart_double_pendulum_direct"
experiment_name: ""
write_interval: auto
checkpoint_interval: auto
# Sequential trainer
# https://skrl.readthedocs.io/en/latest/api/trainers/sequential.html
trainer:
class: SequentialTrainer
timesteps: 4800
environment_info: log
seed: 42
# Models are instantiated using skrl's model instantiator utility
# https://skrl.readthedocs.io/en/latest/api/utils/model_instantiators.html
models:
separate: False
policy: # see gaussian_model parameters
class: GaussianMixin
clip_actions: False
clip_log_std: True
min_log_std: -20.0
max_log_std: 2.0
initial_log_std: 0.0
network:
- name: net
input: STATES
layers: [32, 32]
activations: elu
output: ACTIONS
value: # see deterministic_model parameters
class: DeterministicMixin
clip_actions: False
network:
- name: net
input: STATES
layers: [32, 32]
activations: elu
output: ONE
# Rollout memory
# https://skrl.readthedocs.io/en/latest/api/memories/random.html
memory:
class: RandomMemory
memory_size: -1 # automatically determined (same as agent:rollouts)
# PPO agent configuration (field names are from PPO_DEFAULT_CONFIG)
# https://skrl.readthedocs.io/en/latest/api/agents/ppo.html
agent:
class: PPO
rollouts: 32
learning_epochs: 8
mini_batches: 8
discount_factor: 0.99
lambda: 0.95
learning_rate: 5.0e-04
learning_rate_scheduler: KLAdaptiveLR
learning_rate_scheduler_kwargs:
kl_threshold: 0.008
state_preprocessor: RunningStandardScaler
state_preprocessor_kwargs: null
value_preprocessor: RunningStandardScaler
value_preprocessor_kwargs: null
random_timesteps: 0
learning_starts: 0
grad_norm_clip: 1.0
ratio_clip: 0.2
value_clip: 0.2
clip_predicted_values: True
entropy_loss_scale: 0.0
value_loss_scale: 2.0
kl_threshold: 0.0
rewards_shaper_scale: 0.1
time_limit_bootstrap: False
# logging and checkpoint
experiment:
directory: "cartpole_direct"
experiment_name: ""
write_interval: auto
checkpoint_interval: auto
# Sequential trainer
# https://skrl.readthedocs.io/en/latest/api/trainers/sequential.html
trainer:
class: SequentialTrainer
timesteps: 4800
environment_info: log
# Copyright (c) 2022-2025, The Isaac Lab Project Developers.
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
"""
Python module serving as a project/extension template.
"""
# Register Gym environments.
from .tasks import *
# Register UI extensions.
from .ui_extension_example import *
# Copyright (c) 2022-2025, The Isaac Lab Project Developers.
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
"""Package containing task implementations for the extension."""
##
# Register Gym environments.
##
from isaaclab_tasks.utils import import_packages
# The blacklist is used to prevent importing configs from sub-packages
_BLACKLIST_PKGS = ["utils", ".mdp"]
# Import all configs in this package
import_packages(__name__, _BLACKLIST_PKGS)
# Copyright (c) 2022-2025, The Isaac Lab Project Developers.
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
import gymnasium as gym # noqa: F401
[package]
# Semantic Versioning is used: https://semver.org/
version = "0.1.0"
# Description
category = "isaaclab"
readme = "README.md"
title = "Extension Template"
author = "Isaac Lab Project Developers"
maintainer = "Isaac Lab Project Developers"
description="Extension Template for Isaac Lab"
repository = "https://github.com/isaac-sim/IsaacLab.git"
keywords = ["extension", "template", "isaaclab"]
[dependencies]
"isaaclab" = {}
"isaaclab_assets" = {}
"isaaclab_mimic" = {}
"isaaclab_rl" = {}
"isaaclab_tasks" = {}
# NOTE: Add additional dependencies here
[[python.module]]
name = "{{ name }}"
[isaaclab_settings]
# TODO: Uncomment and list any apt dependencies here.
# If none, leave it commented out.
# apt_deps = ["example_package"]
# TODO: Uncomment and provide path to a ros_ws
# with rosdeps to be installed. If none,
# leave it commented out.
# ros_ws = "path/from/extension_root/to/ros_ws"
Changelog
---------
0.1.0 ({{ date }})
~~~~~~~~~~~~~~~~~~
Added
^^^^^
* Created an initial template for building an extension or project based on Isaac Lab
[build-system]
requires = ["setuptools", "wheel", "toml"]
build-backend = "setuptools.build_meta"
# Copyright (c) 2022-2025, The Isaac Lab Project Developers.
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
"""Installation script for the '{{ name }}' python package."""
import os
import toml
from setuptools import setup
# Obtain the extension data from the extension.toml file
EXTENSION_PATH = os.path.dirname(os.path.realpath(__file__))
# Read the extension.toml file
EXTENSION_TOML_DATA = toml.load(os.path.join(EXTENSION_PATH, "config", "extension.toml"))
# Minimum dependencies required prior to installation
INSTALL_REQUIRES = [
# NOTE: Add dependencies
"psutil",
]
# Installation operation
setup(
name="{{ name }}",
packages=["{{ name }}"],
author=EXTENSION_TOML_DATA["package"]["author"],
maintainer=EXTENSION_TOML_DATA["package"]["maintainer"],
url=EXTENSION_TOML_DATA["package"]["repository"],
version=EXTENSION_TOML_DATA["package"]["version"],
description=EXTENSION_TOML_DATA["package"]["description"],
keywords=EXTENSION_TOML_DATA["package"]["keywords"],
install_requires=INSTALL_REQUIRES,
license="MIT",
include_package_data=True,
python_requires=">=3.10",
classifiers=[
"Natural Language :: English",
"Programming Language :: Python :: 3.10",
"Isaac Sim :: 4.5.0",
],
zip_safe=False,
)
# Copyright (c) 2022-2025, The Isaac Lab Project Developers.
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
import omni.ext
# Functions and vars are available to other extension as usual in python: `example.python_ext.some_public_function(x)`
def some_public_function(x: int):
print("[{{ name }}] some_public_function was called with x: ", x)
return x**x
# Any class derived from `omni.ext.IExt` in top level module (defined in `python.modules` of `extension.toml`) will be
# instantiated when extension gets enabled and `on_startup(ext_id)` will be called. Later when extension gets disabled
# on_shutdown() is called.
class ExampleExtension(omni.ext.IExt):
# ext_id is current extension id. It can be used with extension manager to query additional information, like where
# this extension is located on filesystem.
def on_startup(self, ext_id):
print("[{{ name }}] startup")
self._count = 0
self._window = omni.ui.Window("My Window", width=300, height=300)
with self._window.frame:
with omni.ui.VStack():
label = omni.ui.Label("")
def on_click():
self._count += 1
label.text = f"count: {self._count}"
def on_reset():
self._count = 0
label.text = "empty"
on_reset()
with omni.ui.HStack():
omni.ui.Button("Add", clicked_fn=on_click)
omni.ui.Button("Reset", clicked_fn=on_reset)
def on_shutdown(self):
print("[{{ name }}] shutdown")
# Note: These files are kept for development purposes only.
!tools/launch.template.json
!tools/settings.template.json
!tools/setup_vscode.py
!extensions.json
!tasks.json
# Ignore all other files
.python.env
*.json
{
// See http://go.microsoft.com/fwlink/?LinkId=827846
// for the documentation about the extensions.json format
"recommendations": [
"ms-python.python",
"ms-python.vscode-pylance",
"ban.spellright",
"ms-iot.vscode-ros",
"ms-python.black-formatter",
"ms-python.flake8",
]
}
{
"version": "2.0.0",
"tasks": [
{
"label": "setup_python_env",
"type": "shell",
"linux": {
"command": "${input:isaac_path}/python.sh ${workspaceFolder}/.vscode/tools/setup_vscode.py --isaac_path ${input:isaac_path}"
},
"windows": {
"command": "${input:isaac_path}/python.bat ${workspaceFolder}/.vscode/tools/setup_vscode.py --isaac_path ${input:isaac_path}"
}
}
],
"inputs": [
{
"id": "isaac_path",
"description": "Absolute path to the current Isaac Sim installation. Can be skipped if Isaac Sim installed from pip.",
{% if platform == "win32" %}
"default": "C:/isaacsim",
{% else %}
"default": "${HOME}/isaacsim",
{% endif %}
"type": "promptString"
},
]
}
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
// For standalone script execution
{
"name": "Python: Current File",
"type": "debugpy",
"request": "launch",
"program": "${file}",
"console": "integratedTerminal",
},
{% for specification in specifications %}
{% for rl_library in specification.rl_libraries %}
{% for rl_algorithm in rl_library.algorithms %}
{
"name": "Python: Train {{ specification.task.id }} with {{ rl_library.name }} ({{ rl_algorithm|upper }})",
"type": "debugpy",
"request": "launch",
{% if rl_library.name == "skrl" %}
"args" : ["--task", "{{ specification.task.id }}", "--num_envs", "4096", "--headless", "--algorithm", "{{ rl_algorithm|upper }}"],
{% else %}
"args" : ["--task", "{{ specification.task.id }}", "--num_envs", "4096", "--headless"],
{% endif %}
"program": "${workspaceFolder}/scripts/{{ rl_library.name }}/train.py",
"console": "integratedTerminal",
},
{
"name": "Python: Play {{ specification.task.id }} with {{ rl_library.name }} ({{ rl_algorithm|upper }})",
"type": "debugpy",
"request": "launch",
{% if rl_library.name == "skrl" %}
"args" : ["--task", "{{ specification.task.id }}", "--num_envs", "32", "--algorithm", "{{ rl_algorithm|upper }}"],
{% else %}
"args" : ["--task", "{{ specification.task.id }}", "--num_envs", "32"],
{% endif %}
"program": "${workspaceFolder}/scripts/{{ rl_library.name }}/play.py",
"console": "integratedTerminal",
},
{% endfor %}
{% endfor %}
{% endfor %}
// For script execution inside a Docker
{
"name": "Docker: Current File",
"type": "debugpy",
"request": "launch",
"program": "${file}",
"console": "integratedTerminal",
"env": {
"PYTHONPATH": "${env:PYTHONPATH}:${workspaceFolder}"
}
},
{% for specification in specifications %}
{% for rl_library in specification.rl_libraries %}
{% for rl_algorithm in rl_library.algorithms %}
{
"name": "Docker: Train {{ specification.task.id }} with {{ rl_library.name }} ({{ rl_algorithm|upper }})",
"type": "debugpy",
"request": "launch",
{% if rl_library.name == "skrl" %}
"args" : ["--task", "{{ specification.task.id }}", "--num_envs", "4096", "--headless", "--algorithm", "{{ rl_algorithm|upper }}"],
{% else %}
"args" : ["--task", "{{ specification.task.id }}", "--num_envs", "4096", "--headless"],
{% endif %}
"program": "${workspaceFolder}/scripts/{{ rl_library.name }}/train.py",
"console": "integratedTerminal",
"env": {
"PYTHONPATH": "${env:PYTHONPATH}:${workspaceFolder}"
},
},
{
"name": "Docker: Play {{ specification.task.id }} with {{ rl_library.name }} ({{ rl_algorithm|upper }})",
"type": "debugpy",
"request": "launch",
{% if rl_library.name == "skrl" %}
"args" : ["--task", "{{ specification.task.id }}", "--num_envs", "32", "--algorithm", "{{ rl_algorithm|upper }}"],
{% else %}
"args" : ["--task", "{{ specification.task.id }}", "--num_envs", "32"],
{% endif %}
"program": "${workspaceFolder}/scripts/{{ rl_library.name }}/play.py",
"console": "integratedTerminal",
"env": {
"PYTHONPATH": "${env:PYTHONPATH}:${workspaceFolder}"
},
},
{% endfor %}
{% endfor %}
{% endfor %}
]
}
{
"files.associations": {
"*.tpp": "cpp",
"*.kit": "toml",
"*.rst": "restructuredtext"
},
"editor.rulers": [120],
// files to be ignored by the linter
"files.watcherExclude": {
"**/.git/objects/**": true,
"**/.git/subtree-cache/**": true,
"**/node_modules/**": true,
"**/_isaac_sim/**": true,
"**/_compiler/**": true
},
// Configuration for spelling checker
"spellright.language": [
"en-US-10-1."
],
"spellright.documentTypes": [
"markdown",
"latex",
"plaintext",
"cpp",
"asciidoc",
"python",
"restructuredtext"
],
"cSpell.words": [
"literalinclude",
"linenos",
"instanceable",
"isaacSim",
"jacobians",
"pointcloud",
"ridgeback",
"rllib",
"robomimic",
"teleoperation",
"xform",
"numpy",
"tensordict",
"flatcache",
"physx",
"dpad",
"gamepad",
"linspace",
"upsampled",
"downsampled",
"arange",
"discretization",
"trimesh",
"uninstanceable"
],
// This enables python language server. Seems to work slightly better than jedi:
"python.languageServer": "Pylance",
// We use "black" as a formatter:
"python.formatting.provider": "black",
"python.formatting.blackArgs": ["--line-length", "120"],
// Use flake8 for linting
"python.linting.pylintEnabled": false,
"python.linting.flake8Enabled": true,
"python.linting.flake8Args": [
"--max-line-length=120"
],
// Use docstring generator
"autoDocstring.docstringFormat": "google",
"autoDocstring.guessTypes": true,
// Python environment path
// note: the default interpreter is overridden when user selects a workspace interpreter
// in the status bar. For example, the virtual environment python interpreter
"python.defaultInterpreterPath": "",
// ROS distribution
"ros.distro": "noetic",
// Language specific settings
"[python]": {
"editor.tabSize": 4
},
"[restructuredtext]": {
"editor.tabSize": 2
},
// Python extra paths
// Note: this is filled up when vscode is set up for the first time
"python.analysis.extraPaths": []
}
# Copyright (c) 2022-2025, The Isaac Lab Project Developers.
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
"""This script sets up the vs-code settings for the Isaac Lab project.
This script merges the python.analysis.extraPaths from the "{ISAACSIM_DIR}/.vscode/settings.json" file into
the ".vscode/settings.json" file.
This is necessary because Isaac Sim 2022.2.1 onwards does not add the necessary python packages to the python path
when the "setup_python_env.sh" is run as part of the vs-code launch configuration.
"""
import argparse
import os
import pathlib
import platform
import re
import sys
PROJECT_DIR = pathlib.Path(__file__).parents[2]
"""Path to the the project directory."""
try:
import isaacsim # noqa: F401
isaacsim_dir = os.environ.get("ISAAC_PATH", "")
except ModuleNotFoundError or ImportError:
# Create a parser to get the isaac-sim path
parser = argparse.ArgumentParser(description="Setup the VSCode settings for the project.")
parser.add_argument("--isaac_path", type=str, help="The absolute path to the Isaac Sim installation.")
args = parser.parse_args()
# parse the isaac-sim directory
isaacsim_dir = args.isaac_path
# check if the isaac-sim directory is provided
if not os.path.exists(isaacsim_dir):
raise FileNotFoundError(
f"Could not find the isaac-sim directory: {isaacsim_dir}. Please provide the correct path to the Isaac Sim"
" installation."
)
except EOFError:
print("Unable to trigger EULA acceptance. This is likely due to the script being run in a non-interactive shell.")
print("Please run the script in an interactive shell to accept the EULA.")
print("Skipping the setup of the VSCode settings...")
sys.exit(0)
# check if the isaac-sim directory exists
if not os.path.exists(isaacsim_dir):
raise FileNotFoundError(
f"Could not find the isaac-sim directory: {isaacsim_dir}. There are two possible reasons for this:"
"\n\t1. The Isaac Sim directory does not exist as provided CLI path."
"\n\t2. The script could import the 'isaacsim' package. This could be due to the 'isaacsim' package not being "
"installed in the Python environment.\n"
"\nPlease make sure that the Isaac Sim directory exists or that the 'isaacsim' package is installed."
)
ISAACSIM_DIR = isaacsim_dir
"""Path to the isaac-sim directory."""
def overwrite_python_analysis_extra_paths(isaaclab_settings: str) -> str:
"""Overwrite the python.analysis.extraPaths in the Isaac Lab settings file.
The extraPaths are replaced with the path names from the isaac-sim settings file that exists in the
"{ISAACSIM_DIR}/.vscode/settings.json" file.
If the isaac-sim settings file does not exist, the extraPaths are not overwritten.
Args:
isaaclab_settings: The settings string to use as template.
Returns:
The settings string with overwritten python analysis extra paths.
"""
# isaac-sim settings
isaacsim_vscode_filename = os.path.join(ISAACSIM_DIR, ".vscode", "settings.json")
# we use the isaac-sim settings file to get the python.analysis.extraPaths for kit extensions
# if this file does not exist, we will not add any extra paths
if os.path.exists(isaacsim_vscode_filename):
# read the path names from the isaac-sim settings file
with open(isaacsim_vscode_filename) as f:
vscode_settings = f.read()
# extract the path names
# search for the python.analysis.extraPaths section and extract the contents
settings = re.search(
r"\"python.analysis.extraPaths\": \[.*?\]", vscode_settings, flags=re.MULTILINE | re.DOTALL
)
settings = settings.group(0)
settings = settings.split('"python.analysis.extraPaths": [')[-1]
settings = settings.split("]")[0]
# read the path names from the isaac-sim settings file
path_names = settings.split(",")
path_names = [path_name.strip().strip('"') for path_name in path_names]
path_names = [path_name for path_name in path_names if len(path_name) > 0]
# change the path names to be relative to the Isaac Lab directory
rel_path = os.path.relpath(ISAACSIM_DIR, PROJECT_DIR)
path_names = ['"${workspaceFolder}/' + rel_path + "/" + path_name + '"' for path_name in path_names]
else:
path_names = []
print(
f"[WARN] Could not find Isaac Sim VSCode settings: {isaacsim_vscode_filename}."
"\n\tThis will result in missing 'python.analysis.extraPaths' in the VSCode"
"\n\tsettings, which limits the functionality of the Python language server."
"\n\tHowever, it does not affect the functionality of the Isaac Lab project."
"\n\tWe are working on a fix for this issue with the Isaac Sim team."
)
# add the path names that are in the Isaac Lab extensions directory
isaaclab_extensions = os.listdir(os.path.join(PROJECT_DIR, "source"))
path_names.extend(['"${workspaceFolder}/source/' + ext + '"' for ext in isaaclab_extensions])
# combine them into a single string
path_names = ",\n\t\t".expandtabs(4).join(path_names)
# deal with the path separator being different on Windows and Unix
path_names = path_names.replace("\\", "/")
# replace the path names in the Isaac Lab settings file with the path names parsed
isaaclab_settings = re.sub(
r"\"python.analysis.extraPaths\": \[.*?\]",
'"python.analysis.extraPaths": [\n\t\t'.expandtabs(4) + path_names + "\n\t]".expandtabs(4),
isaaclab_settings,
flags=re.DOTALL,
)
# return the Isaac Lab settings string
return isaaclab_settings
def overwrite_default_python_interpreter(isaaclab_settings: str) -> str:
"""Overwrite the default python interpreter in the Isaac Lab settings file.
The default python interpreter is replaced with the path to the python interpreter used by the
isaac-sim project. This is necessary because the default python interpreter is the one shipped with
isaac-sim.
Args:
isaaclab_settings: The settings string to use as template.
Returns:
The settings string with overwritten default python interpreter.
"""
# read executable name
python_exe = os.path.normpath(sys.executable)
# replace with Isaac Sim's python.sh or python.bat scripts to make sure python with correct
# source paths is set as default
if f"kit{os.sep}python{os.sep}bin{os.sep}python" in python_exe:
# Check if the OS is Windows or Linux to use appropriate shell file
if platform.system() == "Windows":
python_exe = python_exe.replace(f"kit{os.sep}python{os.sep}bin{os.sep}python3", "python.bat")
else:
python_exe = python_exe.replace(f"kit{os.sep}python{os.sep}bin{os.sep}python3", "python.sh")
# replace the default python interpreter in the Isaac Lab settings file with the path to the
# python interpreter in the Isaac Lab directory
isaaclab_settings = re.sub(
r"\"python.defaultInterpreterPath\": \".*?\"",
f'"python.defaultInterpreterPath": "{python_exe}"',
isaaclab_settings,
flags=re.DOTALL,
)
# return the Isaac Lab settings file
return isaaclab_settings
def main():
# Isaac Lab template settings
isaaclab_vscode_template_filename = os.path.join(PROJECT_DIR, ".vscode", "tools", "settings.template.json")
# make sure the Isaac Lab template settings file exists
if not os.path.exists(isaaclab_vscode_template_filename):
raise FileNotFoundError(
f"Could not find the Isaac Lab template settings file: {isaaclab_vscode_template_filename}"
)
# read the Isaac Lab template settings file
with open(isaaclab_vscode_template_filename) as f:
isaaclab_template_settings = f.read()
# overwrite the python.analysis.extraPaths in the Isaac Lab settings file with the path names
isaaclab_settings = overwrite_python_analysis_extra_paths(isaaclab_template_settings)
# overwrite the default python interpreter in the Isaac Lab settings file with the path to the
# python interpreter used to call this script
isaaclab_settings = overwrite_default_python_interpreter(isaaclab_settings)
# add template notice to the top of the file
header_message = (
"// This file is a template and is automatically generated by the setup_vscode.py script.\n"
"// Do not edit this file directly.\n"
"// \n"
f"// Generated from: {isaaclab_vscode_template_filename}\n"
)
isaaclab_settings = header_message + isaaclab_settings
# write the Isaac Lab settings file
isaaclab_vscode_filename = os.path.join(PROJECT_DIR, ".vscode", "settings.json")
with open(isaaclab_vscode_filename, "w") as f:
f.write(isaaclab_settings)
# copy the launch.json file if it doesn't exist
isaaclab_vscode_launch_filename = os.path.join(PROJECT_DIR, ".vscode", "launch.json")
isaaclab_vscode_template_launch_filename = os.path.join(PROJECT_DIR, ".vscode", "tools", "launch.template.json")
if not os.path.exists(isaaclab_vscode_launch_filename):
# read template launch settings
with open(isaaclab_vscode_template_launch_filename) as f:
isaaclab_template_launch_settings = f.read()
# add header
header_message = header_message.replace(
isaaclab_vscode_template_filename, isaaclab_vscode_template_launch_filename
)
isaaclab_launch_settings = header_message + isaaclab_template_launch_settings
# write the Isaac Lab launch settings file
with open(isaaclab_vscode_launch_filename, "w") as f:
f.write(isaaclab_launch_settings)
if __name__ == "__main__":
main()
# Template for Isaac Lab Projects
## Overview
This project/repository serves as a template for building projects or extensions based on Isaac Lab.
It allows you to develop in an isolated environment, outside of the core Isaac Lab repository.
**Key Features:**
- `Isolation` Work outside the core Isaac Lab repository, ensuring that your development efforts remain self-contained.
- `Flexibility` This template is set up to allow your code to be run as an extension in Omniverse.
**Keywords:** extension, template, isaaclab
## 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.
- Clone or copy this project/repository separately from the Isaac Lab installation (i.e. outside the `IsaacLab` directory):
- Using a python interpreter that has Isaac Lab installed, install the library in editable mode using:
```bash
# use 'PATH_TO_isaaclab.sh|bat -p' instead of 'python' if Isaac Lab is not installed in Python venv or conda
python -m pip install -e source/{{ name }}
- Verify that the extension is correctly installed by:
- Listing the available tasks:
Note: It the task name changes, it may be necessary to update the search pattern `"Template-"`
(in the `scripts/list_envs.py` file) so that it can be listed.
```bash
# use 'FULL_PATH_TO_isaaclab.sh|bat -p' instead of 'python' if Isaac Lab is not installed in Python venv or conda
python scripts/list_envs.py
```
- Running a task:
```bash
# use 'FULL_PATH_TO_isaaclab.sh|bat -p' instead of 'python' if Isaac Lab is not installed in Python venv or conda
python scripts/<RL_LIBRARY>/train.py --task=<TASK_NAME>
```
### Set up IDE (Optional)
To setup the IDE, please follow these instructions:
- Run VSCode Tasks, by pressing `Ctrl+Shift+P`, selecting `Tasks: Run Task` and running the `setup_python_env` in the drop down menu.
When running this task, you will be prompted to add the absolute path to your Isaac Sim installation.
If everything executes correctly, it should create a file .python.env in the `.vscode` directory.
The file contains the python paths to all the extensions provided by Isaac Sim and Omniverse.
This helps in indexing all the python modules for intelligent suggestions while writing code.
### Setup as Omniverse Extension (Optional)
We provide an example UI extension that will load upon enabling your extension defined in `source/{{ name }}/{{ name }}/ui_extension_example.py`.
To enable your extension, follow these steps:
1. **Add the search path of this project/repository** to the extension manager:
- Navigate to the extension manager using `Window` -> `Extensions`.
- Click on the **Hamburger Icon** (☰), then go to `Settings`.
- In the `Extension Search Paths`, enter the absolute path to the `source` directory of this project/repository.
- If not already present, in the `Extension Search Paths`, enter the path that leads to Isaac Lab's extension directory directory (`IsaacLab/source`)
- Click on the **Hamburger Icon** (☰), then click `Refresh`.
2. **Search and enable your extension**:
- Find your extension under the `Third Party` category.
- Toggle it to enable your extension.
## Code formatting
We have a pre-commit template to automatically format your code.
To install pre-commit:
```bash
pip install pre-commit
```
Then you can run pre-commit with:
```bash
pre-commit run --all-files
```
## Troubleshooting
### Pylance Missing Indexing of Extensions
In some VsCode versions, the indexing of part of the extensions is missing.
In this case, add the path to your extension in `.vscode/settings.json` under the key `"python.analysis.extraPaths"`.
```json
{
"python.analysis.extraPaths": [
"<path-to-ext-repo>/source/{{ name }}"
]
}
```
### Pylance Crash
If you encounter a crash in `pylance`, it is probable that too many files are indexed and you run out of memory.
A possible solution is to exclude some of omniverse packages that are not used in your project.
To do so, modify `.vscode/settings.json` and comment out packages under the key `"python.analysis.extraPaths"`
Some examples of packages that can likely be excluded are:
```json
"<path-to-isaac-sim>/extscache/omni.anim.*" // Animation packages
"<path-to-isaac-sim>/extscache/omni.kit.*" // Kit UI tools
"<path-to-isaac-sim>/extscache/omni.graph.*" // Graph UI tools
"<path-to-isaac-sim>/extscache/omni.services.*" // Services tools
...
```
###
# General settings
###
# Isaac Lab base image
ISAACLAB_BASE_IMAGE=isaac-lab-base
# The Isaac Lab Extension Template path in the container
DOCKER_ISAACLAB_EXTENSION_TEMPLATE_PATH=/workspace/isaaclab_extension_template
ARG ISAACLAB_BASE_IMAGE_ARG
# we use the basic isaaclab image as the base
FROM ${ISAACLAB_BASE_IMAGE_ARG} AS base
ARG DOCKER_ISAACLAB_EXTENSION_TEMPLATE_PATH_ARG
ENV DOCKER_ISAACLAB_EXTENSION_TEMPLATE_PATH=${DOCKER_ISAACLAB_EXTENSION_TEMPLATE_PATH_ARG}
USER root
# Copy the Isaac Lab Extension Template directory (files to exclude are defined in .dockerignore)
COPY ../ ${DOCKER_ISAACLAB_EXTENSION_TEMPLATE_PATH}
# # Install whatever you need as additional dependencies.
RUN bash -i -c "source ${HOME}/.bashrc && \
cd ${DOCKER_ISAACLAB_EXTENSION_TEMPLATE_PATH}/source/{{ name }} && \
pip install -e ."
# make working directory as the Isaac Lab directory
# this is the default directory when the container is run
WORKDIR /workspace
x-default-isaac-lab-template-environment: &default-isaac-lab-template-environment
- OMNI_KIT_ALLOW_ROOT=1
x-default-isaac-lab-template-deploy: &default-isaac-lab-template-deploy
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities: [ gpu ]
services:
isaac-lab-template:
env_file: .env.base
build:
context: ../
dockerfile: docker/Dockerfile
args:
- ISAACLAB_BASE_IMAGE_ARG=${ISAACLAB_BASE_IMAGE}
- DOCKER_ISAACLAB_EXTENSION_TEMPLATE_PATH_ARG=${DOCKER_ISAACLAB_EXTENSION_TEMPLATE_PATH}
image: isaac-lab-template
container_name: isaac-lab-template
volumes:
- type: bind
source: ../
target: ${DOCKER_ISAACLAB_EXTENSION_TEMPLATE_PATH}
network_mode: host
environment: *default-isaac-lab-template-environment
deploy: *default-isaac-lab-template-deploy
# This is the entrypoint for the container
entrypoint: bash
stdin_open: true
tty: true
# Copyright (c) 2022-2025, The Isaac Lab Project Developers.
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
# Copyright (c) 2022-2025, The Isaac Lab Project Developers.
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
import gymnasium as gym
from . import agents
##
# Register Gym environments.
##
gym.register(
id="{{ task.id }}",
{% if task.workflow.name == "direct" %}
entry_point=f"{__name__}.{{ task.filename }}_env:{{ task.classname }}Env",
{% else %}
entry_point="isaaclab.envs:ManagerBasedRLEnv",
{% endif %}
disable_env_checker=True,
kwargs={
"env_cfg_entry_point": f"{__name__}.{{ task.filename }}_env_cfg:{{ task.classname }}EnvCfg",
{# RL libraries configurations #}
{% for rl_library in rl_libraries %}
{% for algorithm in rl_library.algorithms %}
{# configuration file #}
{% if rl_library.name == "rsl_rl" %}
{% set agent_config = rl_library.name ~ "_" ~ algorithm ~ "_cfg:" ~ algorithm|upper ~ "RunnerCfg" %}
{% else %}
{% set agent_config = rl_library.name ~ "_" ~ algorithm ~ "_cfg.yaml" %}
{% endif %}
{# library configuration #}
{% if algorithm == "ppo" %}
"{{ rl_library.name }}_cfg_entry_point": f"{agents.__name__}:{{ agent_config }}",
{% else %}
"{{ rl_library.name }}_{{ algorithm }}_cfg_entry_point": f"{agents.__name__}:{{ agent_config }}",
{% endif %}
{% endfor %}
{% endfor %}
},
)
# Copyright (c) 2022-2025, The Isaac Lab Project Developers.
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
from __future__ import annotations
import math
import torch
from collections.abc import Sequence
import isaaclab.sim as sim_utils
from isaaclab.assets import Articulation
from isaaclab.envs import DirectMARLEnv
from isaaclab.sim.spawners.from_files import GroundPlaneCfg, spawn_ground_plane
from isaaclab.utils.math import sample_uniform
from .{{ task.filename }}_env_cfg import {{ task.classname }}EnvCfg
class {{ task.classname }}Env(DirectMARLEnv):
cfg: {{ task.classname }}EnvCfg
def __init__(self, cfg: {{ task.classname }}EnvCfg, render_mode: str | None = None, **kwargs):
super().__init__(cfg, render_mode, **kwargs)
self._cart_dof_idx, _ = self.robot.find_joints(self.cfg.cart_dof_name)
self._pole_dof_idx, _ = self.robot.find_joints(self.cfg.pole_dof_name)
self._pendulum_dof_idx, _ = self.robot.find_joints(self.cfg.pendulum_dof_name)
self.joint_pos = self.robot.data.joint_pos
self.joint_vel = self.robot.data.joint_vel
def _setup_scene(self):
self.robot = Articulation(self.cfg.robot_cfg)
# add ground plane
spawn_ground_plane(prim_path="/World/ground", cfg=GroundPlaneCfg())
# clone and replicate
self.scene.clone_environments(copy_from_source=False)
# add articulation to scene
self.scene.articulations["robot"] = self.robot
# add lights
light_cfg = sim_utils.DomeLightCfg(intensity=2000.0, color=(0.75, 0.75, 0.75))
light_cfg.func("/World/Light", light_cfg)
def _pre_physics_step(self, actions: dict[str, torch.Tensor]) -> None:
self.actions = actions
def _apply_action(self) -> None:
self.robot.set_joint_effort_target(
self.actions["cart"] * self.cfg.cart_action_scale, joint_ids=self._cart_dof_idx
)
self.robot.set_joint_effort_target(
self.actions["pendulum"] * self.cfg.pendulum_action_scale, joint_ids=self._pendulum_dof_idx
)
def _get_observations(self) -> dict[str, torch.Tensor]:
pole_joint_pos = normalize_angle(self.joint_pos[:, self._pole_dof_idx[0]].unsqueeze(dim=1))
pendulum_joint_pos = normalize_angle(self.joint_pos[:, self._pendulum_dof_idx[0]].unsqueeze(dim=1))
observations = {
"cart": torch.cat(
(
self.joint_pos[:, self._cart_dof_idx[0]].unsqueeze(dim=1),
self.joint_vel[:, self._cart_dof_idx[0]].unsqueeze(dim=1),
pole_joint_pos,
self.joint_vel[:, self._pole_dof_idx[0]].unsqueeze(dim=1),
),
dim=-1,
),
"pendulum": torch.cat(
(
pole_joint_pos + pendulum_joint_pos,
pendulum_joint_pos,
self.joint_vel[:, self._pendulum_dof_idx[0]].unsqueeze(dim=1),
),
dim=-1,
),
}
return observations
def _get_rewards(self) -> dict[str, torch.Tensor]:
total_reward = compute_rewards(
self.cfg.rew_scale_alive,
self.cfg.rew_scale_terminated,
self.cfg.rew_scale_cart_pos,
self.cfg.rew_scale_cart_vel,
self.cfg.rew_scale_pole_pos,
self.cfg.rew_scale_pole_vel,
self.cfg.rew_scale_pendulum_pos,
self.cfg.rew_scale_pendulum_vel,
self.joint_pos[:, self._cart_dof_idx[0]],
self.joint_vel[:, self._cart_dof_idx[0]],
normalize_angle(self.joint_pos[:, self._pole_dof_idx[0]]),
self.joint_vel[:, self._pole_dof_idx[0]],
normalize_angle(self.joint_pos[:, self._pendulum_dof_idx[0]]),
self.joint_vel[:, self._pendulum_dof_idx[0]],
math.prod(self.terminated_dict.values()),
)
return total_reward
def _get_dones(self) -> tuple[dict[str, torch.Tensor], dict[str, torch.Tensor]]:
self.joint_pos = self.robot.data.joint_pos
self.joint_vel = self.robot.data.joint_vel
time_out = self.episode_length_buf >= self.max_episode_length - 1
out_of_bounds = torch.any(torch.abs(self.joint_pos[:, self._cart_dof_idx]) > self.cfg.max_cart_pos, dim=1)
out_of_bounds = out_of_bounds | torch.any(torch.abs(self.joint_pos[:, self._pole_dof_idx]) > math.pi / 2, dim=1)
terminated = {agent: out_of_bounds for agent in self.cfg.possible_agents}
time_outs = {agent: time_out for agent in self.cfg.possible_agents}
return terminated, time_outs
def _reset_idx(self, env_ids: Sequence[int] | None):
if env_ids is None:
env_ids = self.robot._ALL_INDICES
super()._reset_idx(env_ids)
joint_pos = self.robot.data.default_joint_pos[env_ids]
joint_pos[:, self._pole_dof_idx] += sample_uniform(
self.cfg.initial_pole_angle_range[0] * math.pi,
self.cfg.initial_pole_angle_range[1] * math.pi,
joint_pos[:, self._pole_dof_idx].shape,
joint_pos.device,
)
joint_pos[:, self._pendulum_dof_idx] += sample_uniform(
self.cfg.initial_pendulum_angle_range[0] * math.pi,
self.cfg.initial_pendulum_angle_range[1] * math.pi,
joint_pos[:, self._pendulum_dof_idx].shape,
joint_pos.device,
)
joint_vel = self.robot.data.default_joint_vel[env_ids]
default_root_state = self.robot.data.default_root_state[env_ids]
default_root_state[:, :3] += self.scene.env_origins[env_ids]
self.joint_pos[env_ids] = joint_pos
self.joint_vel[env_ids] = joint_vel
self.robot.write_root_pose_to_sim(default_root_state[:, :7], env_ids)
self.robot.write_root_velocity_to_sim(default_root_state[:, 7:], env_ids)
self.robot.write_joint_state_to_sim(joint_pos, joint_vel, None, env_ids)
@torch.jit.script
def normalize_angle(angle):
return (angle + math.pi) % (2 * math.pi) - math.pi
@torch.jit.script
def compute_rewards(
rew_scale_alive: float,
rew_scale_terminated: float,
rew_scale_cart_pos: float,
rew_scale_cart_vel: float,
rew_scale_pole_pos: float,
rew_scale_pole_vel: float,
rew_scale_pendulum_pos: float,
rew_scale_pendulum_vel: float,
cart_pos: torch.Tensor,
cart_vel: torch.Tensor,
pole_pos: torch.Tensor,
pole_vel: torch.Tensor,
pendulum_pos: torch.Tensor,
pendulum_vel: torch.Tensor,
reset_terminated: torch.Tensor,
):
rew_alive = rew_scale_alive * (1.0 - reset_terminated.float())
rew_termination = rew_scale_terminated * reset_terminated.float()
rew_pole_pos = rew_scale_pole_pos * torch.sum(torch.square(pole_pos).unsqueeze(dim=1), dim=-1)
rew_pendulum_pos = rew_scale_pendulum_pos * torch.sum(
torch.square(pole_pos + pendulum_pos).unsqueeze(dim=1), dim=-1
)
rew_cart_vel = rew_scale_cart_vel * torch.sum(torch.abs(cart_vel).unsqueeze(dim=1), dim=-1)
rew_pole_vel = rew_scale_pole_vel * torch.sum(torch.abs(pole_vel).unsqueeze(dim=1), dim=-1)
rew_pendulum_vel = rew_scale_pendulum_vel * torch.sum(torch.abs(pendulum_vel).unsqueeze(dim=1), dim=-1)
total_reward = {
"cart": rew_alive + rew_termination + rew_pole_pos + rew_cart_vel + rew_pole_vel,
"pendulum": rew_alive + rew_termination + rew_pendulum_pos + rew_pendulum_vel,
}
return total_reward
# Copyright (c) 2022-2025, The Isaac Lab Project Developers.
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
from isaaclab_assets.robots.cart_double_pendulum import CART_DOUBLE_PENDULUM_CFG
from isaaclab.assets import ArticulationCfg
from isaaclab.envs import DirectMARLEnvCfg
from isaaclab.scene import InteractiveSceneCfg
from isaaclab.sim import SimulationCfg
from isaaclab.utils import configclass
@configclass
class {{ task.classname }}EnvCfg(DirectMARLEnvCfg):
# env
decimation = 2
episode_length_s = 5.0
# multi-agent specification and spaces definition
possible_agents = ["cart", "pendulum"]
action_spaces = {"cart": 1, "pendulum": 1}
observation_spaces = {"cart": 4, "pendulum": 3}
state_space = -1
# simulation
sim: SimulationCfg = SimulationCfg(dt=1 / 120, render_interval=decimation)
# robot(s)
robot_cfg: ArticulationCfg = CART_DOUBLE_PENDULUM_CFG.replace(prim_path="/World/envs/env_.*/Robot")
# scene
scene: InteractiveSceneCfg = InteractiveSceneCfg(num_envs=4096, env_spacing=4.0, replicate_physics=True)
# custom parameters/scales
# - controllable joint
cart_dof_name = "slider_to_cart"
pole_dof_name = "cart_to_pole"
pendulum_dof_name = "pole_to_pendulum"
# - action scale
cart_action_scale = 100.0 # [N]
pendulum_action_scale = 50.0 # [Nm]
# - reward scales
rew_scale_alive = 1.0
rew_scale_terminated = -2.0
rew_scale_cart_pos = 0
rew_scale_cart_vel = -0.01
rew_scale_pole_pos = -1.0
rew_scale_pole_vel = -0.01
rew_scale_pendulum_pos = -1.0
rew_scale_pendulum_vel = -0.01
# - reset states/conditions
initial_pendulum_angle_range = [-0.25, 0.25] # pendulum angle sample range on reset [rad]
initial_pole_angle_range = [-0.25, 0.25] # pole angle sample range on reset [rad]
max_cart_pos = 3.0 # reset if cart exceeds this position [m]
# Copyright (c) 2022-2025, The Isaac Lab Project Developers.
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
from __future__ import annotations
import math
import torch
from collections.abc import Sequence
import isaaclab.sim as sim_utils
from isaaclab.assets import Articulation
from isaaclab.envs import DirectRLEnv
from isaaclab.sim.spawners.from_files import GroundPlaneCfg, spawn_ground_plane
from isaaclab.utils.math import sample_uniform
from .{{ task.filename }}_env_cfg import {{ task.classname }}EnvCfg
class {{ task.classname }}Env(DirectRLEnv):
cfg: {{ task.classname }}EnvCfg
def __init__(self, cfg: {{ task.classname }}EnvCfg, render_mode: str | None = None, **kwargs):
super().__init__(cfg, render_mode, **kwargs)
self._cart_dof_idx, _ = self.robot.find_joints(self.cfg.cart_dof_name)
self._pole_dof_idx, _ = self.robot.find_joints(self.cfg.pole_dof_name)
self.joint_pos = self.robot.data.joint_pos
self.joint_vel = self.robot.data.joint_vel
def _setup_scene(self):
self.robot = Articulation(self.cfg.robot_cfg)
# add ground plane
spawn_ground_plane(prim_path="/World/ground", cfg=GroundPlaneCfg())
# clone and replicate
self.scene.clone_environments(copy_from_source=False)
# add articulation to scene
self.scene.articulations["robot"] = self.robot
# add lights
light_cfg = sim_utils.DomeLightCfg(intensity=2000.0, color=(0.75, 0.75, 0.75))
light_cfg.func("/World/Light", light_cfg)
def _pre_physics_step(self, actions: torch.Tensor) -> None:
self.actions = actions.clone()
def _apply_action(self) -> None:
self.robot.set_joint_effort_target(self.actions * self.cfg.action_scale, joint_ids=self._cart_dof_idx)
def _get_observations(self) -> dict:
obs = torch.cat(
(
self.joint_pos[:, self._pole_dof_idx[0]].unsqueeze(dim=1),
self.joint_vel[:, self._pole_dof_idx[0]].unsqueeze(dim=1),
self.joint_pos[:, self._cart_dof_idx[0]].unsqueeze(dim=1),
self.joint_vel[:, self._cart_dof_idx[0]].unsqueeze(dim=1),
),
dim=-1,
)
observations = {"policy": obs}
return observations
def _get_rewards(self) -> torch.Tensor:
total_reward = compute_rewards(
self.cfg.rew_scale_alive,
self.cfg.rew_scale_terminated,
self.cfg.rew_scale_pole_pos,
self.cfg.rew_scale_cart_vel,
self.cfg.rew_scale_pole_vel,
self.joint_pos[:, self._pole_dof_idx[0]],
self.joint_vel[:, self._pole_dof_idx[0]],
self.joint_pos[:, self._cart_dof_idx[0]],
self.joint_vel[:, self._cart_dof_idx[0]],
self.reset_terminated,
)
return total_reward
def _get_dones(self) -> tuple[torch.Tensor, torch.Tensor]:
self.joint_pos = self.robot.data.joint_pos
self.joint_vel = self.robot.data.joint_vel
time_out = self.episode_length_buf >= self.max_episode_length - 1
out_of_bounds = torch.any(torch.abs(self.joint_pos[:, self._cart_dof_idx]) > self.cfg.max_cart_pos, dim=1)
out_of_bounds = out_of_bounds | torch.any(torch.abs(self.joint_pos[:, self._pole_dof_idx]) > math.pi / 2, dim=1)
return out_of_bounds, time_out
def _reset_idx(self, env_ids: Sequence[int] | None):
if env_ids is None:
env_ids = self.robot._ALL_INDICES
super()._reset_idx(env_ids)
joint_pos = self.robot.data.default_joint_pos[env_ids]
joint_pos[:, self._pole_dof_idx] += sample_uniform(
self.cfg.initial_pole_angle_range[0] * math.pi,
self.cfg.initial_pole_angle_range[1] * math.pi,
joint_pos[:, self._pole_dof_idx].shape,
joint_pos.device,
)
joint_vel = self.robot.data.default_joint_vel[env_ids]
default_root_state = self.robot.data.default_root_state[env_ids]
default_root_state[:, :3] += self.scene.env_origins[env_ids]
self.joint_pos[env_ids] = joint_pos
self.joint_vel[env_ids] = joint_vel
self.robot.write_root_pose_to_sim(default_root_state[:, :7], env_ids)
self.robot.write_root_velocity_to_sim(default_root_state[:, 7:], env_ids)
self.robot.write_joint_state_to_sim(joint_pos, joint_vel, None, env_ids)
@torch.jit.script
def compute_rewards(
rew_scale_alive: float,
rew_scale_terminated: float,
rew_scale_pole_pos: float,
rew_scale_cart_vel: float,
rew_scale_pole_vel: float,
pole_pos: torch.Tensor,
pole_vel: torch.Tensor,
cart_pos: torch.Tensor,
cart_vel: torch.Tensor,
reset_terminated: torch.Tensor,
):
rew_alive = rew_scale_alive * (1.0 - reset_terminated.float())
rew_termination = rew_scale_terminated * reset_terminated.float()
rew_pole_pos = rew_scale_pole_pos * torch.sum(torch.square(pole_pos).unsqueeze(dim=1), dim=-1)
rew_cart_vel = rew_scale_cart_vel * torch.sum(torch.abs(cart_vel).unsqueeze(dim=1), dim=-1)
rew_pole_vel = rew_scale_pole_vel * torch.sum(torch.abs(pole_vel).unsqueeze(dim=1), dim=-1)
total_reward = rew_alive + rew_termination + rew_pole_pos + rew_cart_vel + rew_pole_vel
return total_reward
# Copyright (c) 2022-2025, The Isaac Lab Project Developers.
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
from isaaclab_assets.robots.cartpole import CARTPOLE_CFG
from isaaclab.assets import ArticulationCfg
from isaaclab.envs import DirectRLEnvCfg
from isaaclab.scene import InteractiveSceneCfg
from isaaclab.sim import SimulationCfg
from isaaclab.utils import configclass
@configclass
class {{ task.classname }}EnvCfg(DirectRLEnvCfg):
# env
decimation = 2
episode_length_s = 5.0
# - spaces definition
action_space = 1
observation_space = 4
state_space = 0
# simulation
sim: SimulationCfg = SimulationCfg(dt=1 / 120, render_interval=decimation)
# robot(s)
robot_cfg: ArticulationCfg = CARTPOLE_CFG.replace(prim_path="/World/envs/env_.*/Robot")
# scene
scene: InteractiveSceneCfg = InteractiveSceneCfg(num_envs=4096, env_spacing=4.0, replicate_physics=True)
# custom parameters/scales
# - controllable joint
cart_dof_name = "slider_to_cart"
pole_dof_name = "cart_to_pole"
# - action scale
action_scale = 100.0 # [N]
# - reward scales
rew_scale_alive = 1.0
rew_scale_terminated = -2.0
rew_scale_pole_pos = -1.0
rew_scale_cart_vel = -0.01
rew_scale_pole_vel = -0.005
# - reset states/conditions
initial_pole_angle_range = [-0.25, 0.25] # pole angle sample range on reset [rad]
max_cart_pos = 3.0 # reset if cart exceeds this position [m]
# Copyright (c) 2022-2025, The Isaac Lab Project Developers.
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
import math
import isaaclab.sim as sim_utils
from isaaclab.assets import ArticulationCfg, AssetBaseCfg
from isaaclab.envs import ManagerBasedRLEnvCfg
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.utils import configclass
from . import mdp
##
# Pre-defined configs
##
from isaaclab_assets.robots.cartpole import CARTPOLE_CFG # isort:skip
##
# Scene definition
##
@configclass
class {{ task.classname }}SceneCfg(InteractiveSceneCfg):
"""Configuration for a cart-pole scene."""
# ground plane
ground = AssetBaseCfg(
prim_path="/World/ground",
spawn=sim_utils.GroundPlaneCfg(size=(100.0, 100.0)),
)
# robot
robot: ArticulationCfg = CARTPOLE_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot")
# lights
dome_light = AssetBaseCfg(
prim_path="/World/DomeLight",
spawn=sim_utils.DomeLightCfg(color=(0.9, 0.9, 0.9), intensity=500.0),
)
##
# MDP settings
##
@configclass
class ActionsCfg:
"""Action specifications for the MDP."""
joint_effort = mdp.JointEffortActionCfg(asset_name="robot", joint_names=["slider_to_cart"], scale=100.0)
@configclass
class ObservationsCfg:
"""Observation specifications for the MDP."""
@configclass
class PolicyCfg(ObsGroup):
"""Observations for policy group."""
# observation terms (order preserved)
joint_pos_rel = ObsTerm(func=mdp.joint_pos_rel)
joint_vel_rel = ObsTerm(func=mdp.joint_vel_rel)
def __post_init__(self) -> None:
self.enable_corruption = False
self.concatenate_terms = True
# observation groups
policy: PolicyCfg = PolicyCfg()
@configclass
class EventCfg:
"""Configuration for events."""
# reset
reset_cart_position = EventTerm(
func=mdp.reset_joints_by_offset,
mode="reset",
params={
"asset_cfg": SceneEntityCfg("robot", joint_names=["slider_to_cart"]),
"position_range": (-1.0, 1.0),
"velocity_range": (-0.5, 0.5),
},
)
reset_pole_position = EventTerm(
func=mdp.reset_joints_by_offset,
mode="reset",
params={
"asset_cfg": SceneEntityCfg("robot", joint_names=["cart_to_pole"]),
"position_range": (-0.25 * math.pi, 0.25 * math.pi),
"velocity_range": (-0.25 * math.pi, 0.25 * math.pi),
},
)
@configclass
class RewardsCfg:
"""Reward terms for the MDP."""
# (1) Constant running reward
alive = RewTerm(func=mdp.is_alive, weight=1.0)
# (2) Failure penalty
terminating = RewTerm(func=mdp.is_terminated, weight=-2.0)
# (3) Primary task: keep pole upright
pole_pos = RewTerm(
func=mdp.joint_pos_target_l2,
weight=-1.0,
params={"asset_cfg": SceneEntityCfg("robot", joint_names=["cart_to_pole"]), "target": 0.0},
)
# (4) Shaping tasks: lower cart velocity
cart_vel = RewTerm(
func=mdp.joint_vel_l1,
weight=-0.01,
params={"asset_cfg": SceneEntityCfg("robot", joint_names=["slider_to_cart"])},
)
# (5) Shaping tasks: lower pole angular velocity
pole_vel = RewTerm(
func=mdp.joint_vel_l1,
weight=-0.005,
params={"asset_cfg": SceneEntityCfg("robot", joint_names=["cart_to_pole"])},
)
@configclass
class TerminationsCfg:
"""Termination terms for the MDP."""
# (1) Time out
time_out = DoneTerm(func=mdp.time_out, time_out=True)
# (2) Cart out of bounds
cart_out_of_bounds = DoneTerm(
func=mdp.joint_pos_out_of_manual_limit,
params={"asset_cfg": SceneEntityCfg("robot", joint_names=["slider_to_cart"]), "bounds": (-3.0, 3.0)},
)
##
# Environment configuration
##
@configclass
class {{ task.classname }}EnvCfg(ManagerBasedRLEnvCfg):
# Scene settings
scene: {{ task.classname }}SceneCfg = {{ task.classname }}SceneCfg(num_envs=4096, env_spacing=4.0)
# Basic settings
observations: ObservationsCfg = ObservationsCfg()
actions: ActionsCfg = ActionsCfg()
events: EventCfg = EventCfg()
# MDP settings
rewards: RewardsCfg = RewardsCfg()
terminations: TerminationsCfg = TerminationsCfg()
# Post initialization
def __post_init__(self) -> None:
"""Post initialization."""
# general settings
self.decimation = 2
self.episode_length_s = 5
# viewer settings
self.viewer.eye = (8.0, 0.0, 5.0)
# simulation settings
self.sim.dt = 1 / 120
self.sim.render_interval = self.decimation
# Copyright (c) 2022-2025, The Isaac Lab Project Developers.
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
"""This sub-module contains the functions that are specific to the environment."""
from isaaclab.envs.mdp import * # noqa: F401, F403
from .rewards import * # noqa: F401, F403
# Copyright (c) 2022-2025, The Isaac Lab Project Developers.
# 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
from isaaclab.managers import SceneEntityCfg
from isaaclab.utils.math import wrap_to_pi
if TYPE_CHECKING:
from isaaclab.envs import ManagerBasedRLEnv
def joint_pos_target_l2(env: ManagerBasedRLEnv, target: float, asset_cfg: SceneEntityCfg) -> torch.Tensor:
"""Penalize joint position deviation from a target value."""
# extract the used quantities (to enable type-hinting)
asset: Articulation = env.scene[asset_cfg.name]
# wrap the joint positions to (-pi, pi)
joint_pos = wrap_to_pi(asset.data.joint_pos[:, asset_cfg.joint_ids])
# compute the reward
return torch.sum(torch.square(joint_pos - target), dim=1)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment