Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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
148 changes: 109 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,74 @@ 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.\n"
"Internal: 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 +230,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 +277,46 @@ 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