diff --git a/docs/source/overview/developer-guide/template.rst b/docs/source/overview/developer-guide/template.rst index 7c75d27179e..3f0193c65d2 100644 --- a/docs/source/overview/developer-guide/template.rst +++ b/docs/source/overview/developer-guide/template.rst @@ -59,6 +59,32 @@ The generator will guide you in setting up the project/task for your needs by as * Isaac Lab workflows (see :ref:`feature-workflows`). * Reinforcement learning libraries (see :ref:`rl-frameworks`), and algorithms (if the selected libraries support multiple algorithms). +Optional: Include Custom USD Assets (external project only) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If your project involves custom robots or environments with their own USD files, the template generator supports this directly. + +This is especially useful if you want to keep everything self-contained in a single, isolated directory — without having your tasks in the external project and your custom assets elsewhere (e.g., in the main Isaac Lab repository structure). + +To add your own robot: + +1. Place your USD file in:: + + FULL_PATH_TO_PROJECT//source//data/Robots//.usd + +2. Create a corresponding configuration file in:: + + FULL_PATH_TO_PROJECT//source///robots/.py + + following the format described in the :ref:`how-to-write-articulation-config`. + +3. Import your asset configuration in the following file:: + + FULL_PATH_TO_PROJECT//source///robots/__init__.py + +4. Import your robot config in your environment configuration file (e.g., ``*_env_cfg.py``), just like in the Cartpole examples provided by the generated template. + + External project usage (once generated) --------------------------------------- @@ -134,6 +160,48 @@ Here are some general commands to get started with it: python scripts\\train.py --task= +* Run a task with dummy agents. + + These include dummy agents that output zero or random agents. They are useful to ensure that the environments are configured correctly. + + * Zero-action agent + + .. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code-block:: bash + + python scripts/zero_agent.py --task= + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code-block:: batch + + python scripts\zero_agent.py --task= + + * Random-action agent + + .. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code-block:: bash + + python scripts/random_agent.py --task= + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code-block:: batch + + python scripts\random_agent.py --task= + For more details, please follow the instructions in the generated project's ``README.md`` file. Internal task usage (once generated) @@ -185,45 +253,3 @@ Here are some general commands to get started with it: .. code-block:: batch python scripts\reinforcement_learning\\train.py --task= - -* Run a task with dummy agents. - - These include dummy agents that output zero or random agents. They are useful to ensure that the environments are configured correctly. - - * Zero-action agent - - .. tab-set:: - :sync-group: os - - .. tab-item:: :icon:`fa-brands fa-linux` Linux - :sync: linux - - .. code-block:: bash - - python scripts/zero_agent.py --task= - - .. tab-item:: :icon:`fa-brands fa-windows` Windows - :sync: windows - - .. code-block:: batch - - python scripts\zero_agent.py --task= - - * Random-action agent - - .. tab-set:: - :sync-group: os - - .. tab-item:: :icon:`fa-brands fa-linux` Linux - :sync: linux - - .. code-block:: bash - - python scripts/random_agent.py --task= - - .. tab-item:: :icon:`fa-brands fa-windows` Windows - :sync: windows - - .. code-block:: batch - - python scripts\random_agent.py --task= diff --git a/tools/template/cli.py b/tools/template/cli.py index f9480b461d8..97f450fceed 100644 --- a/tools/template/cli.py +++ b/tools/template/cli.py @@ -179,6 +179,18 @@ def main() -> None: ), ) + # custom robot support (if 'external') + use_custom_usd = None + if is_external_project: + use_custom_usd = ( + cli_handler.input_select( + "Do you want to include a custom robot USD file?", + choices=["Yes", "No"], + default="No", + ) + == "Yes" + ) + # Isaac Lab workflow # - show supported workflows and features workflow_table = rich.table.Table(title="RL environment features support according to Isaac Lab workflows") @@ -244,6 +256,7 @@ def main() -> None: "external": is_external_project, "path": project_path, "name": project_name, + "custom_usd": use_custom_usd, "workflows": workflow, "rl_libraries": rl_library_algorithms, } diff --git a/tools/template/generator.py b/tools/template/generator.py index 5055e56dcf3..a2f7ad5d261 100644 --- a/tools/template/generator.py +++ b/tools/template/generator.py @@ -8,6 +8,7 @@ import shutil import subprocess import sys +import urllib.request from datetime import datetime import jinja2 @@ -245,6 +246,146 @@ def _external(specification: dict) -> None: os.path.join(TEMPLATE_DIR, "extension", "__init__workflow"), os.path.join(dir, workflow["name"].replace("-", "_"), "__init__.py"), ) + + # create custom robot structure + if specification.get("custom_usd", False): + print(" |-- Creating custom robot folder structure...") + + import argparse + + from isaaclab.app import AppLauncher + + # create argparser + parser = argparse.ArgumentParser(description="Downloading the assets.") + # append AppLauncher cli args + AppLauncher.add_app_launcher_args(parser) + # parse the arguments + args_cli = parser.parse_args() + args_cli.headless = True + # launch omniverse app + app_launcher = AppLauncher(args_cli) + simulation_app = app_launcher.app + + from isaaclab.utils.assets import ISAACLAB_NUCLEUS_DIR + + # look at workflows once + has_single = any(wf["type"] == "single-agent" for wf in specification["workflows"]) + has_multi = any(wf["type"] == "multi-agent" for wf in specification["workflows"]) + + # only include the assets needed + usd_assets = {} + if has_single: + usd_assets["cartpole"] = [ + ("Robots/Classic/Cartpole", "cartpole.usd"), + ("Robots/Classic/Cartpole/Props", "instanceable_meshes.usd"), + ] + if has_multi: + usd_assets["cart_double_pendulum"] = [ + ("Robots/Classic/CartDoublePendulum", "cart_double_pendulum.usd"), + ("Robots/Classic/CartDoublePendulum/Props", "instanceable_meshes.usd"), + ] + + # download just the ones in usd_assets + for robot, files in usd_assets.items(): + for subfolder, filename in files: + target_dir = os.path.join(project_dir, "source", name, "data", subfolder) + os.makedirs(target_dir, exist_ok=True) + usd_url = f"{ISAACLAB_NUCLEUS_DIR}/{subfolder}/{filename}" + usd_path = os.path.join(target_dir, filename) + + try: + urllib.request.urlretrieve(usd_url, usd_path) + print(f" | |-- Downloaded USD to {usd_path}") + except Exception as e: + print(f" | |-- Failed to download {filename}: {e}") + with open(usd_path, "w") as f: + f.write("# Placeholder for your custom robot USD file.\n") + + # now copy & patch only the robot modules needed + robot_py_dir = os.path.join(project_dir, "source", name, name, "robots") + os.makedirs(robot_py_dir, exist_ok=True) + + def copy_and_patch_robot(robot_file, path): + src = os.path.join(ROOT_DIR, "source", "isaaclab_assets", "isaaclab_assets", "robots", robot_file) + dst = os.path.join(robot_py_dir, robot_file) + shutil.copyfile(src, dst) + _replace_in_file( + [ + ( + "from isaaclab.utils.assets import ISAACLAB_NUCLEUS_DIR", + ( + "from pathlib import Path\n\nTEMPLATE_ASSETS_DATA_DIR =" + ' Path(__file__).resolve().parent.parent.parent / "data"' + ), + ), + ( + 'usd_path=f"{ISAACLAB_NUCLEUS_DIR}/' + path + '"', + 'usd_path=f"{TEMPLATE_ASSETS_DATA_DIR}/' + path + '"', + ), + ], + src=dst, + ) + + if has_single: + copy_and_patch_robot("cartpole.py", "Robots/Classic/Cartpole/cartpole.usd") + if has_multi: + copy_and_patch_robot( + "cart_double_pendulum.py", "Robots/Classic/CartDoublePendulum/cart_double_pendulum.usd" + ) + + # write __init__.py + with open(os.path.join(robot_py_dir, "__init__.py"), "w") as f: + f.write( + """# Copyright (c) 2022-2025, The Isaac Lab Project Developers.\n# All rights reserved.\n#\n# SPDX-License-Identifier: BSD-3-Clause\n\nfrom .cartpole import *\nfrom .cart_double_pendulum import *\n""" + ) + + simulation_app.close() + + # replace import in task config + task_dir = os.path.join(project_dir, "source", name, name, "tasks") + for subpath in [ + os.path.join(task_dir, "direct", name, f"{name}_env_cfg.py"), + os.path.join(task_dir, "manager_based", name, f"{name}_env_cfg.py"), + os.path.join(task_dir, "direct", f"{name}_marl", f"{name}_marl_env_cfg.py"), + ]: + if os.path.exists(subpath): + _replace_in_file( + [ + ( + "from isaaclab_assets.robots.cartpole import CARTPOLE_CFG", + f"from {name}.robots.cartpole import CARTPOLE_CFG", + ), + ( + "from isaaclab_assets.robots.cart_double_pendulum import CART_DOUBLE_PENDULUM_CFG", + f"from {name}.robots.cart_double_pendulum import CART_DOUBLE_PENDULUM_CFG", + ), + ], + src=subpath, + ) + else: + print(subpath) + + # remove USD files interdiction from .gitignore + gitignore_path = os.path.join(project_dir, ".gitignore") + with open(gitignore_path) as f: + lines = f.readlines() + cleaned = [] + skip = False + for line in lines: + if "# No USD files allowed in the repo" in line: + skip = True + continue + if skip and (line.startswith("**/*.usd") or line.startswith("**/*.usd")): + continue + if skip and line.strip() == "": + skip = False + continue + if not skip: + cleaned.append(line) + with open(gitignore_path, "w") as f: + f.writelines(cleaned) + print(" |-- Updated .gitignore to allow USD files (custom_usd=True)") + # - other files dir = os.path.join(project_dir, "source", name, name) template = jinja_env.get_template("extension/ui_extension_example.py") diff --git a/tools/template/templates/external/README.md b/tools/template/templates/external/README.md index eeef1f3f87e..956f92308af 100644 --- a/tools/template/templates/external/README.md +++ b/tools/template/templates/external/README.md @@ -60,6 +60,36 @@ It allows you to develop in an isolated environment, outside of the core Isaac L # use 'FULL_PATH_TO_isaaclab.sh|bat -p' instead of 'python' if Isaac Lab is not installed in Python venv or conda python scripts/random_agent.py --task= ``` +### Include Custom USD Assets (Optional) + + +If your project uses custom robots or environments with their own USD files, the template generator supports setting this up directly. + +This setup ensures everything — tasks, code, and assets — lives in a single, isolated directory instead of splitting files between your external project and the core Isaac Lab repository. + +To add your own robot: + +1. Add your USD asset as: + + ```bash + FULL_PATH_TO_PROJECT//source//data/Robots//.usd + ``` + +2. Create a config file describing your robot as: + + ```bash + FULL_PATH_TO_PROJECT//source///robots/.py + ``` + + Follow the format described in the [How-To Guide](https://isaac-sim.github.io/IsaacLab/main/source/how-to/write_articulation_cfg.html). + +3. Import your asset configuration in the following file: + + ```bash + FULL_PATH_TO_PROJECT//source///robots/__init__.py + ``` + +4. Import your config in the task’s environment configuration file (e.g., `*_env_cfg.py`), just like in the Cartpole examples included in this template. ### Set up IDE (Optional)