Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CONTRIBUTORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ Guidelines for modifications:
* Johnson Sun
* Kaixi Bao
* Kris Wilson
* Krishna Lakhi
* Kourosh Darvish
* Kousheek Chakraborty
* Lionel Gulich
Expand Down
24 changes: 20 additions & 4 deletions isaaclab.bat
Original file line number Diff line number Diff line change
Expand Up @@ -559,11 +559,15 @@ if "%arg%"=="-i" (
) else if "%arg%"=="-n" (
rem run the template generator script
call :extract_python_exe
rem detect non-interactive flag while reconstructing arguments
set "isNonInteractive=0"
set "allArgs="
set "skip="
for %%a in (%*) do (
REM Append each argument to the variable, skip the first one
if defined skip (
set "allArgs=!allArgs! %%a"
if /I "%%~a"=="--non-interactive" set "isNonInteractive=1"
set "allArgs=!allArgs! ^"%%~a^""
) else (
set "skip=1"
)
Expand All @@ -573,16 +577,24 @@ if "%arg%"=="-i" (
echo.
echo [INFO] Running template generator...
echo.
call !python_exe! tools\template\cli.py !allArgs!
if "!isNonInteractive!"=="1" (
call !python_exe! tools\template\cli.py --non-interactive !allArgs!
Comment on lines +580 to +581
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: --non-interactive flag is passed twice when already present in !allArgs!

Suggested change
if "!isNonInteractive!"=="1" (
call !python_exe! tools\template\cli.py --non-interactive !allArgs!
call !python_exe! tools\template\cli.py !allArgs!

) else (
call !python_exe! tools\template\cli.py !allArgs!
)
goto :end
) else if "%arg%"=="--new" (
rem run the template generator script
call :extract_python_exe
rem detect non-interactive flag while reconstructing arguments
set "isNonInteractive=0"
set "allArgs="
set "skip="
for %%a in (%*) do (
REM Append each argument to the variable, skip the first one
if defined skip (
set "allArgs=!allArgs! %%a"
if /I "%%~a"=="--non-interactive" set "isNonInteractive=1"
set "allArgs=!allArgs! ^"%%~a^""
) else (
set "skip=1"
)
Expand All @@ -592,7 +604,11 @@ if "%arg%"=="-i" (
echo.
echo [INFO] Running template generator...
echo.
call !python_exe! tools\template\cli.py !allArgs!
if "!isNonInteractive!"=="1" (
call !python_exe! tools\template\cli.py --non-interactive !allArgs!
Comment on lines +607 to +608
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: --non-interactive flag is passed twice when already present in !allArgs!

Suggested change
if "!isNonInteractive!"=="1" (
call !python_exe! tools\template\cli.py --non-interactive !allArgs!
call !python_exe! tools\template\cli.py !allArgs!

) else (
call !python_exe! tools\template\cli.py !allArgs!
)
goto :end
) else if "%arg%"=="-t" (
rem run the python provided by Isaac Sim
Expand Down
6 changes: 5 additions & 1 deletion isaaclab.sh
Original file line number Diff line number Diff line change
Expand Up @@ -708,7 +708,11 @@ while [[ $# -gt 0 ]]; do
echo "[INFO] Installing template dependencies..."
${pip_command} -q -r ${ISAACLAB_PATH}/tools/template/requirements.txt
echo -e "\n[INFO] Running template generator...\n"
${python_exe} ${ISAACLAB_PATH}/tools/template/cli.py $@
if [[ " $* " == *" --non-interactive "* ]]; then
${python_exe} ${ISAACLAB_PATH}/tools/template/cli.py --non-interactive $@
Comment on lines +711 to +712
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: --non-interactive flag is passed twice when already present in $@

Suggested change
if [[ " $* " == *" --non-interactive "* ]]; then
${python_exe} ${ISAACLAB_PATH}/tools/template/cli.py --non-interactive $@
${python_exe} ${ISAACLAB_PATH}/tools/template/cli.py $@

else
${python_exe} ${ISAACLAB_PATH}/tools/template/cli.py $@
fi
# exit neatly
break
;;
Expand Down
155 changes: 116 additions & 39 deletions tools/template/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#
# SPDX-License-Identifier: BSD-3-Clause

import argparse
import enum
import importlib
import os
Expand Down Expand Up @@ -148,44 +149,79 @@ def main() -> None:
"""Main function to run template generation from CLI."""
cli_handler = CLIHandler()

parser = argparse.ArgumentParser(add_help=True)
parser.add_argument("-n", "--non-interactive", action="store_true")
parser.add_argument("--rl-library", dest="rl_library", type=str, default=None)
parser.add_argument("--rl-algorithm", dest="rl_algorithm", type=str, default=None)
parser.add_argument(
"--task-type",
dest="task_type",
type=str,
choices=["External", "Internal", "external", "internal"],
default=None,
)
parser.add_argument("--project-path", dest="project_path", type=str, default=None)
parser.add_argument("--project-name", dest="project_name", type=str, default=None)
parser.add_argument("--workflow", dest="workflow", type=str, default=None)
args = parser.parse_args()

lab_module = importlib.import_module("isaaclab")
lab_path = os.path.realpath(getattr(lab_module, "__file__", "") or (getattr(lab_module, "__path__", [""])[0]))
is_lab_pip_installed = ("site-packages" in lab_path) or ("dist-packages" in lab_path)

if not is_lab_pip_installed:
# 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"
)
if args.non_interactive:
if args.task_type is not None:
is_external_project = args.task_type.lower() == "external"
else:
is_external_project = True
else:
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.\nInternal: the task is implemented within the Isaac Lab project (in"
" source/isaaclab_tasks)."
),
).lower()
== "external"
)
else:
is_external_project = True

# 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",
)
if args.non_interactive:
project_path = args.project_path
if project_path is None:
raise SystemExit("In non-interactive mode, --project_path is required for External task type.")
if os.path.abspath(project_path).startswith(os.path.abspath(ROOT_DIR)):
raise SystemExit("External project path cannot be within the Isaac Lab project")
else:
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.)"
),
)
if args.non_interactive:
project_name = args.project_name
if project_name is None or not project_name.isidentifier():
raise SystemExit("In non-interactive mode, --project_name is required and must be a valid identifier")
else:
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
Expand All @@ -199,10 +235,23 @@ def main() -> None:
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,
)
if args.non_interactive:
if args.workflow is not None:
selected_workflows = [item.strip() for item in args.workflow.split(",") if item.strip()]
if any(item.lower() == "all" for item in selected_workflows):
workflow = supported_workflows
else:
selected_workflows = [item for item in selected_workflows if item in supported_workflows]
if not selected_workflows:
raise SystemExit("No valid --workflow provided for the selected workflows")
workflow = selected_workflows
else:
workflow = supported_workflows
else:
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]
single_agent_workflow = [item for item in workflow if item["type"] == "single-agent"]
multi_agent_workflow = [item for item in workflow if item["type"] == "multi-agent"]
Expand Down Expand Up @@ -233,20 +282,48 @@ def main() -> None:
cli_handler.output_table(rl_library_table)
# - prompt for RL libraries
supported_rl_libraries = ["rl_games", "rsl_rl", "skrl", "sb3"] if len(single_agent_workflow) else ["skrl"]
selected_rl_libraries = cli_handler.get_choices(
cli_handler.input_checkbox("RL library:", choices=[*supported_rl_libraries, "---", "all"]),
default=supported_rl_libraries,
)
if args.non_interactive:
if args.rl_library is not None:
selected_rl_libraries_raw = [item.strip() for item in args.rl_library.split(",") if item.strip()]
if any(item.lower() == "all" for item in selected_rl_libraries_raw):
selected_rl_libraries = supported_rl_libraries
else:
selected_rl_libraries = [item for item in selected_rl_libraries_raw if item in supported_rl_libraries]
if not selected_rl_libraries:
raise SystemExit("No valid --rl_library provided for the selected workflows")
else:
selected_rl_libraries = supported_rl_libraries
else:
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
algorithms_per_rl_library = get_algorithms_per_rl_library(len(single_agent_workflow), len(multi_agent_workflow))
algorithms_per_rl_library = get_algorithms_per_rl_library(
bool(len(single_agent_workflow)), bool(len(multi_agent_workflow))
)
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]})
if args.non_interactive:
if args.rl_algorithm is not None:
provided_algorithms = [item.strip().lower() for item in args.rl_algorithm.split(",") if item.strip()]
if "all" in provided_algorithms:
selected_algorithms = [item.lower() for item in algorithms]
else:
valid_algorithms = [item for item in provided_algorithms if item in [a.lower() for a in algorithms]]
if not valid_algorithms:
raise SystemExit(f"No valid --rl_algorithm provided for library '{rl_library}'")
selected_algorithms = valid_algorithms
else:
selected_algorithms = [item.lower() for item in algorithms]
Comment on lines +307 to +318
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Same --rl_algorithm value is reused for all RL libraries

When multiple RL libraries are selected, the same --rl_algorithm value is used for all of them. Each library has different supported algorithms, so this causes invalid algorithm selection.

Example: User passes --rl-library=rl_games,skrl --rl-algorithm=PPO. The code validates PPO against rl_games algorithms (valid), but then reuses the same PPO validation for skrl algorithms, which may fail if skrl has different algorithm names.

else:
if len(algorithms) > 1:
algorithms = cli_handler.get_choices(
cli_handler.input_checkbox(f"RL algorithms for {rl_library}:", choices=[*algorithms, "---", "all"]),
default=algorithms,
)
selected_algorithms = [item.lower() for item in algorithms]
rl_library_algorithms.append({"name": rl_library, "algorithms": selected_algorithms})

specification = {
"external": is_external_project,
Expand Down
Loading