first commit
This commit is contained in:
commit
4a8b860f87
|
|
@ -0,0 +1,31 @@
|
|||
# ignore .git related folders
|
||||
.git/
|
||||
.github/
|
||||
.gitignore
|
||||
# ignore docs
|
||||
docs/
|
||||
# copy in licenses folder to the container
|
||||
!docs/licenses/
|
||||
# ignore logs
|
||||
**/logs/
|
||||
**/runs/
|
||||
**/output/*
|
||||
**/outputs/*
|
||||
**/videos/*
|
||||
**/wandb/*
|
||||
*.tmp
|
||||
# ignore docker
|
||||
docker/cluster/exports/
|
||||
docker/.container.cfg
|
||||
# ignore recordings
|
||||
recordings/
|
||||
# ignore __pycache__
|
||||
**/__pycache__/
|
||||
**/*.egg-info/
|
||||
# ignore isaac sim symlink
|
||||
_isaac_sim
|
||||
# Docker history
|
||||
docker/.isaac-lab-docker-history
|
||||
# ignore uv environment
|
||||
env_isaaclab
|
||||
tools/wheel_builder/build/
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
*.usd filter=lfs diff=lfs merge=lfs -text
|
||||
*.usda filter=lfs diff=lfs merge=lfs -text
|
||||
*.psd filter=lfs diff=lfs merge=lfs -text
|
||||
*.hdr filter=lfs diff=lfs merge=lfs -text
|
||||
*.dae filter=lfs diff=lfs merge=lfs -text
|
||||
*.mtl filter=lfs diff=lfs merge=lfs -text
|
||||
*.obj filter=lfs diff=lfs merge=lfs -text
|
||||
*.gif filter=lfs diff=lfs merge=lfs -text
|
||||
*.mp4 filter=lfs diff=lfs merge=lfs -text
|
||||
*.pt filter=lfs diff=lfs merge=lfs -text
|
||||
*.jit filter=lfs diff=lfs merge=lfs -text
|
||||
*.hdf5 filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
source/isaaclab_tasks/test/golden_images/**/*.png filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
*.bat text eol=crlf
|
||||
*.sh text eol=lf
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
# C++
|
||||
**/cmake-build*/
|
||||
**/build*/
|
||||
**/*.so
|
||||
**/*.log*
|
||||
|
||||
# Omniverse
|
||||
**/*.dmp
|
||||
**/.thumbs
|
||||
|
||||
# No USD files allowed in the repo
|
||||
**/*.usd
|
||||
**/*.usda
|
||||
**/*.usdc
|
||||
**/*.usdz
|
||||
|
||||
# Python
|
||||
.DS_Store
|
||||
**/*.egg-info/
|
||||
**/__pycache__/
|
||||
**/.pytest_cache/
|
||||
**/*.pyc
|
||||
**/*.pb
|
||||
|
||||
# Docker/Singularity
|
||||
**/*.sif
|
||||
docker/cluster/exports/
|
||||
docker/.container.cfg
|
||||
|
||||
# IDE
|
||||
**/.idea/
|
||||
**/.vscode/
|
||||
# Don't ignore the top-level .vscode directory as it is
|
||||
# used to configure VS Code settings
|
||||
!.vscode
|
||||
|
||||
# Outputs
|
||||
**/output/*
|
||||
**/outputs/*
|
||||
**/videos/*
|
||||
**/wandb/*
|
||||
**/.neptune/*
|
||||
docker/artifacts/
|
||||
*.tmp
|
||||
|
||||
# Doc Outputs
|
||||
**/docs/_build/*
|
||||
**/generated/*
|
||||
|
||||
# Isaac-Sim packman
|
||||
_isaac_sim*
|
||||
_repo
|
||||
_build
|
||||
.lastformat
|
||||
|
||||
# RL-Games
|
||||
**/runs/*
|
||||
**/logs/*
|
||||
**/recordings/*
|
||||
|
||||
# Pre-Trained Checkpoints
|
||||
/.pretrained_checkpoints/
|
||||
|
||||
# Teleop Recorded Dataset
|
||||
/datasets/
|
||||
|
||||
# Tests
|
||||
/tests/
|
||||
|
||||
# Docker history
|
||||
.isaac-lab-docker-history
|
||||
|
||||
# TacSL sensor
|
||||
**/tactile_record/*
|
||||
**/gelsight_r15_data/*
|
||||
|
||||
# No benchmarks output
|
||||
/benchmarks/
|
||||
|
||||
# Ruff cache
|
||||
**/.ruff_cache/
|
||||
|
||||
# Dev-time files, generated stuff
|
||||
**/__*
|
||||
|
||||
# Isaac Lab CI environments in native mode
|
||||
**/_isaaclab_install_ci_*
|
||||
|
||||
# Superpowers (Claude Code plugin artifacts)
|
||||
docs/superpowers/
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md).
|
||||
# All rights reserved.
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.14.10
|
||||
hooks:
|
||||
# Run the linter
|
||||
- id: ruff
|
||||
args: ["--fix"]
|
||||
# Run the formatter
|
||||
- id: ruff-format
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v6.0.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: check-symlinks
|
||||
- id: destroyed-symlinks
|
||||
- id: check-added-large-files
|
||||
args: ["--maxkb=2000"] # restrict files more than 2 MB. Should use git-lfs instead.
|
||||
- id: check-yaml
|
||||
- id: check-merge-conflict
|
||||
- id: check-case-conflict
|
||||
- id: check-executables-have-shebangs
|
||||
- id: check-toml
|
||||
- id: end-of-file-fixer
|
||||
- id: check-shebang-scripts-are-executable
|
||||
- id: detect-private-key
|
||||
- id: debug-statements
|
||||
- repo: https://github.com/codespell-project/codespell
|
||||
rev: v2.4.1
|
||||
hooks:
|
||||
- id: codespell
|
||||
additional_dependencies:
|
||||
- tomli
|
||||
exclude: "CONTRIBUTORS.md|docs/source/setup/walkthrough/concepts_env_design.rst"
|
||||
# FIXME: Figure out why this is getting stuck under VPN.
|
||||
# - repo: https://github.com/RobertCraigie/pyright-python
|
||||
# rev: v1.1.315
|
||||
# hooks:
|
||||
# - id: pyright
|
||||
- repo: https://github.com/Lucas-C/pre-commit-hooks
|
||||
rev: v1.5.5
|
||||
hooks:
|
||||
- id: insert-license
|
||||
files: \.(pyi?|ya?ml)$
|
||||
args:
|
||||
# - --remove-header # Remove existing license headers. Useful when updating license.
|
||||
- --license-filepath
|
||||
- .github/LICENSE_HEADER.txt
|
||||
- --use-current-year
|
||||
exclude: "source/isaaclab_mimic/|scripts/imitation_learning/isaaclab_mimic/"
|
||||
# Apache 2.0 license for mimic files
|
||||
- repo: https://github.com/Lucas-C/pre-commit-hooks
|
||||
rev: v1.5.5
|
||||
hooks:
|
||||
- id: insert-license
|
||||
files: ^(source/isaaclab_mimic|scripts/imitation_learning/isaaclab_mimic)/.*\.py$
|
||||
args:
|
||||
# - --remove-header # Remove existing license headers. Useful when updating license.
|
||||
- --license-filepath
|
||||
- .github/LICENSE_HEADER_MIMIC.txt
|
||||
- --use-current-year
|
||||
- repo: https://github.com/pre-commit/pygrep-hooks
|
||||
rev: v1.10.0
|
||||
hooks:
|
||||
- id: rst-backticks
|
||||
- id: rst-directive-colons
|
||||
- id: rst-inline-touching-normal
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
# 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
|
||||
|
|
@ -0,0 +1,200 @@
|
|||
# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md).
|
||||
# All rights reserved.
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
"""This script 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 re
|
||||
import subprocess
|
||||
import sys
|
||||
import os
|
||||
import pathlib
|
||||
|
||||
|
||||
ISAACLAB_DIR = pathlib.Path(__file__).parents[2]
|
||||
"""Path to the Isaac Lab directory."""
|
||||
|
||||
# Try to find IsaacSim dir
|
||||
_isaacsim_probe = subprocess.run(
|
||||
[sys.executable, "-c", "import isaacsim; import os; print(os.environ.get('ISAAC_PATH', ''))"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
# avoid EULA prompt
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
if _isaacsim_probe.returncode == 0 and _isaacsim_probe.stdout.strip():
|
||||
isaacsim_dir = _isaacsim_probe.stdout.strip()
|
||||
else:
|
||||
isaacsim_dir = os.path.join(ISAACLAB_DIR, "_isaac_sim")
|
||||
|
||||
# check if the isaac-sim directory exists
|
||||
if not os.path.exists(isaacsim_dir):
|
||||
print(
|
||||
f"[WARN] Could not find the isaac-sim directory: {isaacsim_dir}."
|
||||
"\n\tIsaac Sim does not appear to be installed. VS Code settings will be generated"
|
||||
"\n\twithout Isaac Sim extra paths."
|
||||
)
|
||||
isaacsim_dir = ""
|
||||
|
||||
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 ISAACSIM_DIR and 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, ISAACLAB_DIR)
|
||||
path_names = ['"${workspaceFolder}/' + rel_path + "/" + path_name + '"' for path_name in path_names]
|
||||
else:
|
||||
path_names = []
|
||||
|
||||
# add the path names that are in the Isaac Lab extensions directory
|
||||
isaaclab_extensions = os.listdir(os.path.join(ISAACLAB_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 = sys.executable.replace("\\", "/")
|
||||
|
||||
# We make an exception for replacing the default interpreter if the
|
||||
# path (/kit/python/bin/python3) indicates that we are using a local/container
|
||||
# installation of IsaacSim. We will preserve the calling script as the default, python.sh.
|
||||
# We want to use python.sh because it modifies LD_LIBRARY_PATH and PYTHONPATH
|
||||
# (among other envars) that we need for all of our dependencies to be accessible.
|
||||
if "kit/python/bin/python3" in python_exe:
|
||||
return isaaclab_settings
|
||||
# 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(ISAACLAB_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(ISAACLAB_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(ISAACLAB_DIR, ".vscode", "launch.json")
|
||||
isaaclab_vscode_template_launch_filename = os.path.join(ISAACLAB_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()
|
||||
|
|
@ -0,0 +1,135 @@
|
|||
# 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 or uv installation as it simplifies calling Python scripts from the terminal.
|
||||
|
||||
- Clone or copy this project/repository separately from the Isaac Lab installation (i.e. outside the `IsaacLab` directory):
|
||||
|
||||
- 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/go2Demo
|
||||
|
||||
- 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>
|
||||
```
|
||||
|
||||
- Running 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
|
||||
|
||||
```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/zero_agent.py --task=<TASK_NAME>
|
||||
```
|
||||
- Random-action agent
|
||||
|
||||
```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/random_agent.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/go2Demo/go2Demo/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/go2Demo"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 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
|
||||
...
|
||||
```
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
@echo off
|
||||
chcp 65001 >nul
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
echo ============================================================
|
||||
echo 脚本: git-force-push.bat
|
||||
echo 功能: 将本地分支强制推送到远程,让远程与本地完全一致
|
||||
echo ============================================================
|
||||
|
||||
REM ---------- 检查是否在 Git 仓库 ----------
|
||||
git rev-parse --is-inside-work-tree >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
echo [错误] 当前目录不是 Git 仓库
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM ---------- 获取当前分支 ----------
|
||||
for /f %%i in ('git branch --show-current') do set CURRENT_BRANCH=%%i
|
||||
if "%CURRENT_BRANCH%"=="" (
|
||||
echo [错误] 当前处于 HEAD 分离状态,请先切换到具体分支。
|
||||
exit /b 1
|
||||
)
|
||||
echo [信息] 当前分支:%CURRENT_BRANCH%
|
||||
|
||||
REM ---------- 拉取远程最新信息 ----------
|
||||
echo [信息] 正在获取远程最新引用...
|
||||
git fetch origin
|
||||
|
||||
REM ---------- 检查本地是否有未提交更改 ----------
|
||||
git diff --quiet >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
echo [警告] 检测到本地有未提交的更改(已暂存或未暂存)。
|
||||
echo [警告] 这些更改不会被推送,但会保留在工作区。
|
||||
set /p CONTINUE="是否继续强制推送?(y/N): "
|
||||
if /i not "!CONTINUE!"=="y" (
|
||||
echo [信息] 操作已取消。
|
||||
exit /b 0
|
||||
)
|
||||
)
|
||||
|
||||
REM ---------- 检查本地是否落后于远程 ----------
|
||||
for /f %%i in ('git rev-list --count @..@{u} 2^>nul') do set AHEAD=%%i
|
||||
if not defined AHEAD set AHEAD=0
|
||||
if %AHEAD% gtr 0 (
|
||||
echo [警告] 远程有 %AHEAD% 个新提交,本地落后于远程。
|
||||
echo [警告] 强制推送会覆盖这些提交,请确认这不是他人刚提交的代码!
|
||||
)
|
||||
|
||||
REM ---------- 选择推送模式 ----------
|
||||
echo 请选择推送模式:
|
||||
echo 1) --force-with-lease (推荐,安全覆盖)
|
||||
echo 2) --force (强制覆盖,极度危险)
|
||||
set /p MODE="请输入数字 (1 或 2,默认 1): "
|
||||
if "%MODE%"=="" set MODE=1
|
||||
if "%MODE%"=="2" (
|
||||
set FORCE_FLAG=--force
|
||||
echo [警告] 您选择了 --force,这将无条件覆盖远程分支!
|
||||
) else (
|
||||
set FORCE_FLAG=--force-with-lease
|
||||
echo [信息] 您选择了 --force-with-lease,安全模式。
|
||||
)
|
||||
|
||||
REM ---------- 最终确认 ----------
|
||||
set /p CONFIRM="确认将本地分支 %CURRENT_BRANCH% 强制推送到远程?请输入 'yes' 继续: "
|
||||
if /i not "%CONFIRM%"=="yes" (
|
||||
echo [信息] 操作已取消。
|
||||
exit /b 0
|
||||
)
|
||||
|
||||
REM ---------- 执行推送 ----------
|
||||
echo [信息] 正在执行: git push %FORCE_FLAG% origin %CURRENT_BRANCH%
|
||||
git push %FORCE_FLAG% origin %CURRENT_BRANCH%
|
||||
if errorlevel 1 (
|
||||
echo [错误] 推送失败,请检查网络或权限。
|
||||
exit /b %errorlevel%
|
||||
)
|
||||
|
||||
echo [成功] 推送完成!远程 origin/%CURRENT_BRANCH% 已与本地保持一致。
|
||||
endlocal
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
#!/bin/bash
|
||||
# ============================================================
|
||||
# 脚本名称: git-force-push.sh
|
||||
# 功能描述: 将本地分支强制推送到远程,让远程与本地完全一致
|
||||
# 使用场景: 本地代码正确,远程有错误或需强制更新
|
||||
# 安全机制: 默认使用 --force-with-lease,并检查远程新提交
|
||||
# ============================================================
|
||||
|
||||
set -e
|
||||
|
||||
# ---------- 颜色 ----------
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
# ---------- 1. 检查是否在 Git 仓库 ----------
|
||||
if ! git rev-parse --is-inside-work-tree > /dev/null 2>&1; then
|
||||
echo -e "${RED}错误:当前目录不是 Git 仓库${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ---------- 2. 获取当前分支 ----------
|
||||
CURRENT_BRANCH=$(git branch --show-current)
|
||||
if [ -z "$CURRENT_BRANCH" ]; then
|
||||
echo -e "${RED}错误:当前处于 HEAD 分离状态,请先切换到具体分支。${NC}"
|
||||
exit 1
|
||||
fi
|
||||
echo -e "${BLUE}当前分支:${CURRENT_BRANCH}${NC}"
|
||||
|
||||
# ---------- 3. 先拉取远程最新信息(但不会合并) ----------
|
||||
echo -e "${BLUE}正在获取远程最新引用...${NC}"
|
||||
git fetch origin
|
||||
|
||||
# ---------- 4. 检查本地是否有未提交的更改 ----------
|
||||
if ! git diff --quiet || ! git diff --cached --quiet; then
|
||||
echo -e "${YELLOW}⚠️ 检测到本地有未提交的更改(已暂存或未暂存)。${NC}"
|
||||
echo -e "${YELLOW}这些更改不会被推送,但会保留在工作区。${NC}"
|
||||
read -p "是否继续强制推送?(y/N) " -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo -e "${GREEN}操作已取消。${NC}"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# ---------- 5. 检查本地是否落后于远程(存在远程新提交) ----------
|
||||
LOCAL_COMMIT=$(git rev-parse @)
|
||||
REMOTE_COMMIT=$(git rev-parse @{u} 2>/dev/null || echo "")
|
||||
if [ -z "$REMOTE_COMMIT" ]; then
|
||||
echo -e "${YELLOW}⚠️ 当前分支未设置上游,将使用 --set-upstream 建立跟踪。${NC}"
|
||||
fi
|
||||
BEHIND=$(git rev-list --count @{u}..@ 2>/dev/null || echo "0")
|
||||
AHEAD=$(git rev-list --count @..@{u} 2>/dev/null || echo "0")
|
||||
|
||||
if [ "$AHEAD" -gt 0 ]; then
|
||||
echo -e "${YELLOW}⚠️ 远程有 $AHEAD 个新提交,本地落后于远程。${NC}"
|
||||
echo -e "${YELLOW}强制推送会覆盖这些提交,请确认这不是他人刚提交的代码!${NC}"
|
||||
fi
|
||||
|
||||
# ---------- 6. 选择强制推送模式 ----------
|
||||
echo -e "${BLUE}请选择推送模式:${NC}"
|
||||
echo " 1) --force-with-lease (推荐,安全覆盖,会检查远程是否有未知新提交)"
|
||||
echo " 2) --force (强制覆盖,忽略远程所有内容,极度危险)"
|
||||
read -p "请输入数字 (1 或 2,默认 1): " MODE
|
||||
MODE=${MODE:-1}
|
||||
|
||||
if [ "$MODE" = "2" ]; then
|
||||
FORCE_FLAG="--force"
|
||||
echo -e "${RED}您选择了 --force,这将无条件覆盖远程分支!${NC}"
|
||||
else
|
||||
FORCE_FLAG="--force-with-lease"
|
||||
echo -e "${GREEN}您选择了 --force-with-lease,安全模式。${NC}"
|
||||
fi
|
||||
|
||||
# ---------- 7. 最终确认 ----------
|
||||
read -p "确认将本地分支 '$CURRENT_BRANCH' 强制推送到远程?请输入 'yes' 继续: " CONFIRM
|
||||
if [ "$CONFIRM" != "yes" ]; then
|
||||
echo -e "${GREEN}操作已取消。${NC}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ---------- 8. 执行推送 ----------
|
||||
echo -e "${BLUE}正在执行: git push $FORCE_FLAG origin $CURRENT_BRANCH${NC}"
|
||||
git push $FORCE_FLAG origin $CURRENT_BRANCH
|
||||
|
||||
# ---------- 9. 结果反馈 ----------
|
||||
echo -e "${GREEN}✅ 推送完成!远程 'origin/$CURRENT_BRANCH' 已与本地保持一致。${NC}"
|
||||
|
|
@ -0,0 +1,266 @@
|
|||
# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md).
|
||||
# All rights reserved.
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
[project]
|
||||
name = "isaaclab-dev"
|
||||
version = "0.1.0"
|
||||
description = "Isaac Lab source checkout development environment."
|
||||
requires-python = ">=3.12,<3.13"
|
||||
dependencies = [
|
||||
"isaaclab",
|
||||
"isaaclab-assets",
|
||||
"isaaclab-contrib",
|
||||
"isaaclab-experimental",
|
||||
"isaaclab-newton[all]",
|
||||
"isaaclab-ov",
|
||||
"isaaclab-ovphysx",
|
||||
"isaaclab-physx[newton]",
|
||||
"isaaclab-ppisp",
|
||||
"isaaclab-rl[rsl-rl]",
|
||||
"isaaclab-tasks",
|
||||
"isaaclab-tasks-experimental",
|
||||
"isaaclab-visualizers",
|
||||
"torch==2.10.0",
|
||||
"torchaudio==2.10.0",
|
||||
"torchvision==0.25.0",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
contrib = [
|
||||
"isaaclab-contrib",
|
||||
]
|
||||
mimic = [
|
||||
"isaaclab-mimic",
|
||||
]
|
||||
newton = [
|
||||
"isaaclab-newton[all]",
|
||||
"isaaclab-physx[newton]",
|
||||
"isaaclab-visualizers[newton]",
|
||||
]
|
||||
ov = [
|
||||
"isaaclab-ovphysx[ovphysx]",
|
||||
]
|
||||
rl = [
|
||||
"isaaclab-rl[rsl-rl]",
|
||||
]
|
||||
rl-all = [
|
||||
"isaaclab-rl[all]",
|
||||
]
|
||||
rtx = [
|
||||
"isaaclab-ov[ovrtx]",
|
||||
]
|
||||
all = [
|
||||
"isaaclab-mimic",
|
||||
"isaaclab-newton[all]",
|
||||
"isaaclab-physx[newton]",
|
||||
"isaaclab-rl[all]",
|
||||
"isaaclab-visualizers[all]",
|
||||
]
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 120
|
||||
target-version = "py310"
|
||||
|
||||
# Exclude directories
|
||||
extend-exclude = [
|
||||
"logs",
|
||||
"_isaac_sim",
|
||||
".vscode",
|
||||
"_*",
|
||||
".git",
|
||||
]
|
||||
|
||||
[tool.ruff.lint]
|
||||
# Enable flake8 rules and other useful ones
|
||||
select = [
|
||||
"E", # pycodestyle errors
|
||||
"W", # pycodestyle warnings
|
||||
"F", # pyflakes
|
||||
"I", # isort
|
||||
"UP", # pyupgrade
|
||||
"C90", # mccabe complexity
|
||||
# "D", # pydocstyle
|
||||
"SIM", # flake8-simplify
|
||||
"RET", # flake8-return
|
||||
]
|
||||
|
||||
# Ignore specific rules (matching your flake8 config)
|
||||
ignore = [
|
||||
"E402", # Module level import not at top of file
|
||||
"D401", # First line should be in imperative mood
|
||||
"RET504", # Unnecessary variable assignment before return statement
|
||||
"RET505", # Unnecessary elif after return statement
|
||||
"SIM102", # Use a single if-statement instead of nested if-statements
|
||||
"SIM103", # Return the negated condition directly
|
||||
"SIM108", # Use ternary operator instead of if-else statement
|
||||
"SIM117", # Merge with statements for context managers
|
||||
"SIM118", # Use {key} in {dict} instead of {key} in {dict}.keys()
|
||||
"UP006", # Use 'dict' instead of 'Dict' type annotation
|
||||
"UP018", # Unnecessary `float` call (rewrite as a literal)
|
||||
]
|
||||
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
"__init__.py" = ["F401"] # Allow unused imports in __init__.py files
|
||||
|
||||
[tool.ruff.lint.mccabe]
|
||||
max-complexity = 30
|
||||
|
||||
[tool.ruff.lint.pydocstyle]
|
||||
convention = "google"
|
||||
|
||||
[tool.ruff.lint.isort]
|
||||
|
||||
# Custom import sections with separate sections for each Isaac Lab extension
|
||||
section-order = [
|
||||
"future",
|
||||
"standard-library",
|
||||
"third-party",
|
||||
# Group omniverse extensions separately since they are run-time dependencies
|
||||
# which are pulled in by Isaac Lab extensions
|
||||
"omniverse-extensions",
|
||||
# Group Isaac Lab extensions together since they are all part of the Isaac Lab project
|
||||
"isaaclab",
|
||||
"isaaclab-contrib",
|
||||
"isaaclab-rl",
|
||||
"isaaclab-mimic",
|
||||
"isaaclab-tasks",
|
||||
"isaaclab-assets",
|
||||
# First-party is reserved for project templates
|
||||
"first-party",
|
||||
"local-folder",
|
||||
]
|
||||
|
||||
[tool.ruff.lint.isort.sections]
|
||||
# Define what belongs in each custom section
|
||||
|
||||
"omniverse-extensions" = [
|
||||
"isaacsim",
|
||||
"omni",
|
||||
"pxr",
|
||||
"carb",
|
||||
"usdrt",
|
||||
"Semantics",
|
||||
"curobo",
|
||||
]
|
||||
|
||||
"isaaclab" = ["isaaclab"]
|
||||
"isaaclab-assets" = ["isaaclab_assets"]
|
||||
"isaaclab-contrib" = ["isaaclab_contrib"]
|
||||
"isaaclab-rl" = ["isaaclab_rl"]
|
||||
"isaaclab-mimic" = ["isaaclab_mimic"]
|
||||
"isaaclab-tasks" = ["isaaclab_tasks"]
|
||||
|
||||
[tool.ruff.format]
|
||||
|
||||
docstring-code-format = true
|
||||
|
||||
[tool.pyright]
|
||||
|
||||
include = ["source", "scripts"]
|
||||
exclude = [
|
||||
"**/__pycache__",
|
||||
"**/_isaac_sim",
|
||||
"**/docs",
|
||||
"**/logs",
|
||||
".git",
|
||||
".vscode",
|
||||
]
|
||||
|
||||
typeCheckingMode = "basic"
|
||||
pythonVersion = "3.12"
|
||||
pythonPlatform = "Linux"
|
||||
enableTypeIgnoreComments = true
|
||||
|
||||
# This is required as the CI pre-commit does not download the module (i.e. numpy, torch, prettytable)
|
||||
# Therefore, we have to ignore missing imports
|
||||
reportMissingImports = "none"
|
||||
# This is required to ignore for type checks of modules with stubs missing.
|
||||
reportMissingModuleSource = "none" # -> most common: prettytable in mdp managers
|
||||
|
||||
reportGeneralTypeIssues = "none" # -> raises 218 errors (usage of literal MISSING in dataclasses)
|
||||
reportOptionalMemberAccess = "warning" # -> raises 8 errors
|
||||
reportPrivateUsage = "warning"
|
||||
|
||||
|
||||
[tool.codespell]
|
||||
skip = '*.usd,*.usda,*.usdz,*.svg,*.png,_isaac_sim*,*.bib,*.css,*/_build'
|
||||
quiet-level = 0
|
||||
# the world list should always have words in lower case
|
||||
ignore-words-list = "haa,slq,collapsable,buss,reacher,thirdparty,segway"
|
||||
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
|
||||
markers = [
|
||||
"isaacsim_ci: mark test to run in isaacsim ci",
|
||||
]
|
||||
|
||||
# Add pypi.nvidia.com so that `uv pip install isaaclab[isaacsim]` works without --extra-index-url.
|
||||
# Pip still needs "--extra-index-url https://pypi.nvidia.com".
|
||||
[[tool.uv.index]]
|
||||
url = "https://pypi.nvidia.com"
|
||||
explicit = false
|
||||
|
||||
[[tool.uv.index]]
|
||||
name = "pytorch-cu128"
|
||||
url = "https://download.pytorch.org/whl/cu128"
|
||||
explicit = true
|
||||
|
||||
[[tool.uv.index]]
|
||||
name = "pytorch-cu130"
|
||||
url = "https://download.pytorch.org/whl/cu130"
|
||||
explicit = true
|
||||
|
||||
# Some NVIDIA-hosted dependencies have mismatched versions across pypi.nvidia.com
|
||||
# and PyPI. unsafe-best-match lets uv resolve the correct version from any index,
|
||||
# and prerelease=allow covers packages that only publish pre-release wheels.
|
||||
[tool.uv]
|
||||
index-strategy = "unsafe-best-match"
|
||||
prerelease = "allow"
|
||||
override-dependencies = ["numpy>=2"]
|
||||
python-preference = "only-managed"
|
||||
package = false
|
||||
|
||||
[tool.uv.sources]
|
||||
isaaclab = { path = "source/isaaclab", editable = true }
|
||||
"isaaclab-assets" = { path = "source/isaaclab_assets", editable = true }
|
||||
"isaaclab-contrib" = { path = "source/isaaclab_contrib", editable = true }
|
||||
"isaaclab-experimental" = { path = "source/isaaclab_experimental", editable = true }
|
||||
"isaaclab-mimic" = { path = "source/isaaclab_mimic", editable = true }
|
||||
"isaaclab-newton" = { path = "source/isaaclab_newton", editable = true }
|
||||
"isaaclab-ov" = { path = "source/isaaclab_ov", editable = true }
|
||||
"isaaclab-ovphysx" = { path = "source/isaaclab_ovphysx", editable = true }
|
||||
"isaaclab-physx" = { path = "source/isaaclab_physx", editable = true }
|
||||
"isaaclab-ppisp" = { path = "source/isaaclab_ppisp", editable = true }
|
||||
"isaaclab-rl" = { path = "source/isaaclab_rl", editable = true }
|
||||
"isaaclab-tasks" = { path = "source/isaaclab_tasks", editable = true }
|
||||
"isaaclab-tasks-experimental" = { path = "source/isaaclab_tasks_experimental", editable = true }
|
||||
"isaaclab-teleop" = { path = "source/isaaclab_teleop", editable = true }
|
||||
"isaaclab-visualizers" = { path = "source/isaaclab_visualizers", editable = true }
|
||||
torch = [
|
||||
{ index = "pytorch-cu128", marker = "sys_platform == 'linux' and platform_machine == 'x86_64'" },
|
||||
{ index = "pytorch-cu128", marker = "sys_platform == 'linux' and platform_machine == 'AMD64'" },
|
||||
{ index = "pytorch-cu128", marker = "sys_platform == 'win32'" },
|
||||
{ index = "pytorch-cu130", marker = "sys_platform == 'linux' and platform_machine == 'aarch64'" },
|
||||
{ index = "pytorch-cu130", marker = "sys_platform == 'linux' and platform_machine == 'arm64'" },
|
||||
]
|
||||
torchaudio = [
|
||||
{ index = "pytorch-cu128", marker = "sys_platform == 'linux' and platform_machine == 'x86_64'" },
|
||||
{ index = "pytorch-cu128", marker = "sys_platform == 'linux' and platform_machine == 'AMD64'" },
|
||||
{ index = "pytorch-cu128", marker = "sys_platform == 'win32'" },
|
||||
{ index = "pytorch-cu130", marker = "sys_platform == 'linux' and platform_machine == 'aarch64'" },
|
||||
{ index = "pytorch-cu130", marker = "sys_platform == 'linux' and platform_machine == 'arm64'" },
|
||||
]
|
||||
torchvision = [
|
||||
{ index = "pytorch-cu128", marker = "sys_platform == 'linux' and platform_machine == 'x86_64'" },
|
||||
{ index = "pytorch-cu128", marker = "sys_platform == 'linux' and platform_machine == 'AMD64'" },
|
||||
{ index = "pytorch-cu128", marker = "sys_platform == 'win32'" },
|
||||
{ index = "pytorch-cu130", marker = "sys_platform == 'linux' and platform_machine == 'aarch64'" },
|
||||
{ index = "pytorch-cu130", marker = "sys_platform == 'linux' and platform_machine == 'arm64'" },
|
||||
]
|
||||
|
||||
[tool.uv.pip]
|
||||
index-strategy = "unsafe-best-match"
|
||||
prerelease = "allow"
|
||||
|
|
@ -0,0 +1,135 @@
|
|||
# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md).
|
||||
# All rights reserved.
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
"""
|
||||
Script to print all the available environments in Isaac Lab.
|
||||
|
||||
The script iterates over all registered environments and stores the details in a table.
|
||||
It prints the name of the environment, the entry point and the config file.
|
||||
|
||||
All the environments are registered in the `go2Demo` extension. They start
|
||||
with `Isaac` in their name.
|
||||
"""
|
||||
|
||||
"""Launch Isaac Sim Simulator first."""
|
||||
|
||||
import argparse
|
||||
import contextlib
|
||||
|
||||
from isaaclab.app import AppLauncher
|
||||
|
||||
# add argparse arguments
|
||||
parser = argparse.ArgumentParser(description="List Isaac Lab environments.")
|
||||
parser.add_argument("--keyword", type=str, default=None, help="Keyword to filter environments.")
|
||||
parser.add_argument(
|
||||
"--show_presets",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help=(
|
||||
"Show available preset selectors for each environment. "
|
||||
"Presets are grouped by selector type: physics (physics=NAME), "
|
||||
"renderer (renderer=NAME), and domain (presets=NAME)."
|
||||
),
|
||||
)
|
||||
# parse the arguments
|
||||
args_cli = parser.parse_args()
|
||||
|
||||
# launch omniverse app
|
||||
app_launcher = AppLauncher(headless=True)
|
||||
simulation_app = app_launcher.app
|
||||
|
||||
|
||||
"""Rest everything follows."""
|
||||
|
||||
import gymnasium as gym
|
||||
from prettytable import PrettyTable
|
||||
|
||||
import go2Demo.tasks # noqa: F401
|
||||
|
||||
# PLACEHOLDER: Extension template (do not remove this comment)
|
||||
with contextlib.suppress(ImportError):
|
||||
import go2Demo.tasks_experimental # noqa: F401
|
||||
|
||||
|
||||
def _format_presets(preset_map: dict | None) -> str:
|
||||
"""Format a preset map returned by :func:`enumerate_task_presets` into a human-readable string.
|
||||
|
||||
Args:
|
||||
preset_map: Mapping of :class:`~go2Demo.utils.preset_target.PresetTarget`
|
||||
to sorted preset name lists, or ``None`` when the env cfg could not be loaded.
|
||||
|
||||
Returns:
|
||||
A multi-line string with one line per non-empty selector category, or a
|
||||
short placeholder when no presets are available or the cfg failed to load.
|
||||
"""
|
||||
if preset_map is None:
|
||||
return "(unavailable)"
|
||||
from go2Demo.utils.preset_target import PresetTarget
|
||||
|
||||
lines = []
|
||||
labels = {
|
||||
PresetTarget.PHYSICS: "physics",
|
||||
PresetTarget.RENDERER: "renderer",
|
||||
PresetTarget.DOMAIN: "domain",
|
||||
}
|
||||
for target, label in labels.items():
|
||||
names = preset_map.get(target, [])
|
||||
if names:
|
||||
lines.append(f"{label}: {', '.join(names)}")
|
||||
return "\n".join(lines) if lines else "(none)"
|
||||
|
||||
|
||||
def main():
|
||||
"""Print all environments registered in `go2Demo` extension."""
|
||||
# Collect matching task specs first so we can enumerate presets in one pass.
|
||||
task_specs = [
|
||||
spec
|
||||
for spec in gym.registry.values()
|
||||
if "Template-" in spec.id and (args_cli.keyword is None or args_cli.keyword in spec.id)
|
||||
]
|
||||
|
||||
if args_cli.show_presets:
|
||||
from go2Demo.utils.preset_cli import enumerate_task_presets
|
||||
|
||||
table = PrettyTable(["S. No.", "Task Name", "Entry Point", "Config", "Presets"])
|
||||
table.title = "Available Environments in Isaac Lab"
|
||||
table.align["Task Name"] = "l"
|
||||
table.align["Entry Point"] = "l"
|
||||
table.align["Config"] = "l"
|
||||
table.align["Presets"] = "l"
|
||||
|
||||
for index, spec in enumerate(task_specs):
|
||||
preset_map = enumerate_task_presets(spec.id)
|
||||
table.add_row(
|
||||
[
|
||||
index + 1,
|
||||
spec.id,
|
||||
spec.entry_point,
|
||||
spec.kwargs["env_cfg_entry_point"],
|
||||
_format_presets(preset_map),
|
||||
]
|
||||
)
|
||||
else:
|
||||
table = PrettyTable(["S. No.", "Task Name", "Entry Point", "Config"])
|
||||
table.title = "Available Environments in Isaac Lab"
|
||||
table.align["Task Name"] = "l"
|
||||
table.align["Entry Point"] = "l"
|
||||
table.align["Config"] = "l"
|
||||
|
||||
for index, spec in enumerate(task_specs):
|
||||
table.add_row([index + 1, spec.id, spec.entry_point, spec.kwargs["env_cfg_entry_point"]])
|
||||
|
||||
print(table)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
# run the main function
|
||||
main()
|
||||
except Exception as e:
|
||||
raise e
|
||||
finally:
|
||||
# close the app
|
||||
simulation_app.close()
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md).
|
||||
# All rights reserved.
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
"""Script to an environment with random action agent."""
|
||||
|
||||
import argparse
|
||||
import contextlib
|
||||
import sys
|
||||
|
||||
import gymnasium as gym
|
||||
import torch
|
||||
|
||||
import isaaclab_tasks # noqa: F401
|
||||
|
||||
with contextlib.suppress(ImportError):
|
||||
import isaaclab_tasks_experimental # noqa: F401
|
||||
from isaaclab_tasks.utils import (
|
||||
add_launcher_args,
|
||||
launch_simulation,
|
||||
resolve_task_config,
|
||||
setup_preset_cli,
|
||||
)
|
||||
|
||||
# add argparse arguments
|
||||
parser = argparse.ArgumentParser(description="Random agent for Isaac Lab environments.")
|
||||
parser.add_argument(
|
||||
"--disable_fabric", action="store_true", default=False, help="Disable fabric and use USD I/O operations."
|
||||
)
|
||||
parser.add_argument("--num_envs", type=int, default=None, help="Number of environments to simulate.")
|
||||
parser.add_argument("--task", type=str, default=None, help="Name of the task.")
|
||||
# append AppLauncher cli args
|
||||
add_launcher_args(parser)
|
||||
# simple agents should open Kit visualizer by default
|
||||
parser.set_defaults(visualizer=["kit"])
|
||||
args_cli, hydra_args = setup_preset_cli(parser)
|
||||
sys.argv = [sys.argv[0]] + hydra_args
|
||||
|
||||
import go2Demo.tasks # noqa: F401
|
||||
|
||||
|
||||
def main():
|
||||
"""Random actions agent with Isaac Lab environment."""
|
||||
|
||||
torch.manual_seed(42)
|
||||
|
||||
# parse configuration via Hydra (supports preset selection, e.g. env.sim.physics=newton_mjwarp)
|
||||
env_cfg, _ = resolve_task_config(args_cli.task, "")
|
||||
|
||||
with launch_simulation(env_cfg, args_cli):
|
||||
# override with CLI arguments
|
||||
env_cfg.scene.num_envs = args_cli.num_envs if args_cli.num_envs is not None else env_cfg.scene.num_envs
|
||||
env_cfg.sim.device = args_cli.device if args_cli.device is not None else env_cfg.sim.device
|
||||
if args_cli.disable_fabric:
|
||||
env_cfg.sim.use_fabric = False
|
||||
|
||||
# create environment
|
||||
env = gym.make(args_cli.task, cfg=env_cfg)
|
||||
|
||||
# print info (this is vectorized environment)
|
||||
print(f"[INFO]: Gym observation space: {env.observation_space}")
|
||||
print(f"[INFO]: Gym action space: {env.action_space}")
|
||||
# reset environment
|
||||
env.reset()
|
||||
# simulate environment
|
||||
sim = env.unwrapped.sim
|
||||
while True:
|
||||
if sim.visualizers:
|
||||
# visualizer mode: run until the visualizer window is closed
|
||||
if not any(v.is_running() and not v.is_closed for v in sim.visualizers):
|
||||
break
|
||||
# run everything in inference mode
|
||||
with torch.inference_mode():
|
||||
# sample actions from -1 to 1
|
||||
actions = 2 * torch.rand(env.action_space.shape, device=env.unwrapped.device) - 1
|
||||
# apply actions
|
||||
env.step(actions)
|
||||
|
||||
# close the simulator
|
||||
env.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# run the main function
|
||||
main()
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md).
|
||||
# All rights reserved.
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import random
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from isaaclab_rl.rsl_rl import RslRlBaseRunnerCfg
|
||||
|
||||
|
||||
def add_rsl_rl_args(parser: argparse.ArgumentParser):
|
||||
"""Add RSL-RL arguments to the parser.
|
||||
|
||||
Args:
|
||||
parser: The parser to add the arguments to.
|
||||
"""
|
||||
# create a new argument group
|
||||
arg_group = parser.add_argument_group("rsl_rl", description="Arguments for RSL-RL agent.")
|
||||
# -- experiment arguments
|
||||
arg_group.add_argument(
|
||||
"--experiment_name", type=str, default=None, help="Name of the experiment folder where logs will be stored."
|
||||
)
|
||||
arg_group.add_argument("--run_name", type=str, default=None, help="Run name suffix to the log directory.")
|
||||
# -- load arguments
|
||||
arg_group.add_argument("--resume", action="store_true", default=False, help="Whether to resume from a checkpoint.")
|
||||
arg_group.add_argument("--load_run", type=str, default=None, help="Name of the run folder to resume from.")
|
||||
arg_group.add_argument("--checkpoint", type=str, default=None, help="Checkpoint file to resume from.")
|
||||
# -- logger arguments
|
||||
arg_group.add_argument(
|
||||
"--logger", type=str, default=None, choices={"wandb", "tensorboard", "neptune"}, help="Logger module to use."
|
||||
)
|
||||
arg_group.add_argument(
|
||||
"--log_project_name", type=str, default=None, help="Name of the logging project when using wandb or neptune."
|
||||
)
|
||||
|
||||
|
||||
def parse_rsl_rl_cfg(task_name: str, args_cli: argparse.Namespace) -> RslRlBaseRunnerCfg:
|
||||
"""Parse configuration for RSL-RL agent based on inputs.
|
||||
|
||||
Args:
|
||||
task_name: The name of the environment.
|
||||
args_cli: The command line arguments.
|
||||
|
||||
Returns:
|
||||
The parsed configuration for RSL-RL agent based on inputs.
|
||||
"""
|
||||
from isaaclab_tasks.utils.parse_cfg import load_cfg_from_registry
|
||||
|
||||
# load the default configuration
|
||||
rslrl_cfg: RslRlBaseRunnerCfg = load_cfg_from_registry(task_name, "rsl_rl_cfg_entry_point")
|
||||
rslrl_cfg = update_rsl_rl_cfg(rslrl_cfg, args_cli)
|
||||
return rslrl_cfg
|
||||
|
||||
|
||||
def update_rsl_rl_cfg(agent_cfg: RslRlBaseRunnerCfg, args_cli: argparse.Namespace):
|
||||
"""Update configuration for RSL-RL agent based on inputs.
|
||||
|
||||
Args:
|
||||
agent_cfg: The configuration for RSL-RL agent.
|
||||
args_cli: The command line arguments.
|
||||
|
||||
Returns:
|
||||
The updated configuration for RSL-RL agent based on inputs.
|
||||
"""
|
||||
# override the default configuration with CLI arguments
|
||||
if hasattr(args_cli, "seed") and args_cli.seed is not None:
|
||||
# randomly sample a seed if seed = -1
|
||||
if args_cli.seed == -1:
|
||||
args_cli.seed = random.randint(0, 10000)
|
||||
agent_cfg.seed = args_cli.seed
|
||||
if args_cli.resume is not None:
|
||||
agent_cfg.resume = args_cli.resume
|
||||
if args_cli.load_run is not None:
|
||||
agent_cfg.load_run = args_cli.load_run
|
||||
if args_cli.checkpoint is not None:
|
||||
agent_cfg.load_checkpoint = args_cli.checkpoint
|
||||
if args_cli.experiment_name is not None:
|
||||
agent_cfg.experiment_name = args_cli.experiment_name
|
||||
if args_cli.run_name is not None:
|
||||
agent_cfg.run_name = args_cli.run_name
|
||||
if args_cli.logger is not None:
|
||||
agent_cfg.logger = args_cli.logger
|
||||
# set the project name for wandb and neptune
|
||||
if agent_cfg.logger in {"wandb", "neptune"} and args_cli.log_project_name:
|
||||
agent_cfg.wandb_project = args_cli.log_project_name
|
||||
agent_cfg.neptune_project = args_cli.log_project_name
|
||||
|
||||
return agent_cfg
|
||||
|
|
@ -0,0 +1,251 @@
|
|||
# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md).
|
||||
# All rights reserved.
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
"""Script to play a checkpoint if an RL agent from RSL-RL."""
|
||||
|
||||
import warnings
|
||||
|
||||
warnings.warn(
|
||||
"scripts/reinforcement_learning/rsl_rl/play.py is deprecated. Use "
|
||||
"`./isaaclab.sh play --rl_library rsl_rl --task <TASK>` instead. "
|
||||
"Example: `./isaaclab.sh play --rl_library rsl_rl --task Isaac-Cartpole-v0`.",
|
||||
DeprecationWarning,
|
||||
stacklevel=1,
|
||||
)
|
||||
|
||||
import argparse
|
||||
import contextlib
|
||||
import importlib.metadata as metadata
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
import gymnasium as gym
|
||||
import torch
|
||||
from packaging import version
|
||||
from rsl_rl.runners import DistillationRunner, OnPolicyRunner
|
||||
|
||||
from isaaclab.envs import DirectMARLEnvCfg, DirectRLEnvCfg, ManagerBasedRLEnvCfg
|
||||
from isaaclab.utils.assets import retrieve_file_path
|
||||
from isaaclab.utils.dict import print_dict
|
||||
from isaaclab.utils.seed import configure_seed
|
||||
from isaaclab.utils.string import list_intersection, string_to_callable
|
||||
|
||||
from isaaclab_rl.rsl_rl import (
|
||||
RslRlBaseRunnerCfg,
|
||||
RslRlVecEnvWrapper,
|
||||
export_policy_as_jit,
|
||||
export_policy_as_onnx,
|
||||
handle_deprecated_rsl_rl_cfg,
|
||||
)
|
||||
from isaaclab_rl.utils.pretrained_checkpoint import get_published_pretrained_checkpoint
|
||||
|
||||
import isaaclab_tasks # noqa: F401
|
||||
from isaaclab_tasks.utils import (
|
||||
add_launcher_args,
|
||||
get_checkpoint_path,
|
||||
launch_simulation,
|
||||
setup_preset_cli,
|
||||
)
|
||||
from isaaclab_tasks.utils.hydra import hydra_task_config
|
||||
|
||||
# local imports
|
||||
import cli_args # isort: skip
|
||||
|
||||
import go2Demo.tasks # noqa: F401
|
||||
with contextlib.suppress(ImportError):
|
||||
import isaaclab_tasks_experimental # noqa: F401
|
||||
|
||||
# -- argparse ----------------------------------------------------------------
|
||||
parser = argparse.ArgumentParser(description="Train an RL agent with RSL-RL.")
|
||||
parser.add_argument("--video", action="store_true", default=True, help="Record videos during training.")
|
||||
parser.add_argument("--video_length", type=int, default=2000, help="Length of the recorded video (in steps).")
|
||||
parser.add_argument(
|
||||
"--disable_fabric", action="store_true", default=False, help="Disable fabric and use USD I/O operations."
|
||||
)
|
||||
parser.add_argument("--num_envs", type=int, default=None, help="Number of environments to simulate.")
|
||||
parser.add_argument("--task", type=str, default=None, help="Name of the task.")
|
||||
parser.add_argument(
|
||||
"--agent", type=str, default="rsl_rl_cfg_entry_point", help="Name of the RL agent configuration entry point."
|
||||
)
|
||||
parser.add_argument("--seed", type=int, default=None, help="Seed used for the environment")
|
||||
parser.add_argument(
|
||||
"--use_pretrained_checkpoint",
|
||||
action="store_true",
|
||||
help="Use the pre-trained checkpoint from Nucleus.",
|
||||
)
|
||||
parser.add_argument("--real-time", action="store_true", default=False, help="Run in real-time, if possible.")
|
||||
parser.add_argument("--external_callback", default=None, help="Fully qualified path to an externally defined callback.")
|
||||
cli_args.add_rsl_rl_args(parser)
|
||||
add_launcher_args(parser)
|
||||
args_cli, remaining_args = setup_preset_cli(parser)
|
||||
|
||||
if args_cli.video:
|
||||
args_cli.enable_cameras = True
|
||||
|
||||
|
||||
# Call an external callback if requested. This gives opportunity to external code to register the environments
|
||||
# The function is expected to return a list of arguments that were not consumed by the callback.
|
||||
remaining_args_env_registration = None
|
||||
if args_cli.external_callback:
|
||||
external_callback_function = string_to_callable(args_cli.external_callback, separator=".")
|
||||
remaining_args_env_registration = external_callback_function()
|
||||
|
||||
# clear out sys.argv for Hydra
|
||||
# The remaining arguments are the arguments that were not consumed by both this scripts
|
||||
# argparser and (optionally) the external callback function. Both sides of this
|
||||
# intersection are pre-fold (the callback reads the user's original sys.argv), so
|
||||
# preset tokens like ``physics=NAME`` compare correctly here. Fold runs after.
|
||||
remaining_args = list_intersection(remaining_args, remaining_args_env_registration)
|
||||
sys.argv = [sys.argv[0]] + remaining_args
|
||||
|
||||
# Check for installed RSL-RL version
|
||||
installed_version = metadata.version("rsl-rl-lib")
|
||||
|
||||
|
||||
@hydra_task_config(args_cli.task, args_cli.agent)
|
||||
def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agent_cfg: RslRlBaseRunnerCfg):
|
||||
"""Play with RSL-RL agent."""
|
||||
with launch_simulation(env_cfg, args_cli):
|
||||
# grab task name for checkpoint path
|
||||
task_name = args_cli.task.split(":")[-1]
|
||||
train_task_name = task_name.replace("-Play", "")
|
||||
|
||||
# override configurations with non-hydra CLI arguments
|
||||
agent_cfg = cli_args.update_rsl_rl_cfg(agent_cfg, args_cli)
|
||||
env_cfg.scene.num_envs = args_cli.num_envs if args_cli.num_envs is not None else env_cfg.scene.num_envs
|
||||
|
||||
# handle deprecated configurations
|
||||
agent_cfg = handle_deprecated_rsl_rl_cfg(agent_cfg, installed_version)
|
||||
|
||||
# set the environment seed
|
||||
# note: certain randomizations occur in the environment initialization so we set the seed here
|
||||
env_cfg.seed = agent_cfg.seed
|
||||
env_cfg.sim.device = args_cli.device if args_cli.device is not None else env_cfg.sim.device
|
||||
|
||||
# specify directory for logging experiments
|
||||
log_root_path = os.path.join("logs", "rsl_rl", agent_cfg.experiment_name)
|
||||
log_root_path = os.path.abspath(log_root_path)
|
||||
print(f"[INFO] Loading experiment from directory: {log_root_path}")
|
||||
if args_cli.use_pretrained_checkpoint:
|
||||
resume_path = get_published_pretrained_checkpoint("rsl_rl", train_task_name)
|
||||
if not resume_path:
|
||||
print("[INFO] Unfortunately a pre-trained checkpoint is currently unavailable for this task.")
|
||||
return
|
||||
elif args_cli.checkpoint:
|
||||
resume_path = retrieve_file_path(args_cli.checkpoint)
|
||||
else:
|
||||
resume_path = get_checkpoint_path(log_root_path, agent_cfg.load_run, agent_cfg.load_checkpoint)
|
||||
|
||||
log_dir = os.path.dirname(resume_path)
|
||||
|
||||
# set the log directory for the environment
|
||||
env_cfg.log_dir = log_dir
|
||||
|
||||
# create isaac environment
|
||||
env = gym.make(args_cli.task, cfg=env_cfg, render_mode="rgb_array" if args_cli.video else None)
|
||||
|
||||
# convert to single-agent instance if required by the RL algorithm
|
||||
if isinstance(env.unwrapped.cfg, DirectMARLEnvCfg):
|
||||
from isaaclab.envs import multi_agent_to_single_agent
|
||||
|
||||
env = multi_agent_to_single_agent(env)
|
||||
|
||||
# wrap for video recording
|
||||
if args_cli.video:
|
||||
video_kwargs = {
|
||||
"video_folder": os.path.join(log_dir, "videos", "play"),
|
||||
"step_trigger": lambda step: step == 0,
|
||||
"video_length": args_cli.video_length,
|
||||
"disable_logger": True,
|
||||
}
|
||||
print("[INFO] Recording videos during training.")
|
||||
print_dict(video_kwargs, nesting=4)
|
||||
env = gym.wrappers.RecordVideo(env, **video_kwargs)
|
||||
|
||||
# wrap around environment for rsl-rl
|
||||
env = RslRlVecEnvWrapper(env, clip_actions=agent_cfg.clip_actions)
|
||||
|
||||
print(f"[INFO]: Loading model checkpoint from: {resume_path}")
|
||||
# load previously trained model
|
||||
if agent_cfg.class_name == "OnPolicyRunner":
|
||||
runner = OnPolicyRunner(env, agent_cfg.to_dict(), log_dir=None, device=agent_cfg.device)
|
||||
elif agent_cfg.class_name == "DistillationRunner":
|
||||
runner = DistillationRunner(env, agent_cfg.to_dict(), log_dir=None, device=agent_cfg.device)
|
||||
else:
|
||||
raise ValueError(f"Unsupported runner class: {agent_cfg.class_name}")
|
||||
# configure_seed must be called after runner construction so that PyTorch deterministic settings
|
||||
# do not interfere with the runner's internal initialization.
|
||||
if args_cli.deterministic:
|
||||
configure_seed(env_cfg.seed, True)
|
||||
runner.load(resume_path)
|
||||
|
||||
# obtain the trained policy for inference
|
||||
policy = runner.get_inference_policy(device=env.unwrapped.device)
|
||||
|
||||
# export the trained policy to JIT and ONNX formats
|
||||
export_model_dir = os.path.join(os.path.dirname(resume_path), "exported")
|
||||
|
||||
if version.parse(installed_version) >= version.parse("4.0.0"):
|
||||
# use the new export functions for rsl-rl >= 4.0.0
|
||||
runner.export_policy_to_jit(path=export_model_dir, filename="policy.pt")
|
||||
runner.export_policy_to_onnx(path=export_model_dir, filename="policy.onnx")
|
||||
policy_nn = None # Not needed for rsl-rl >= 4.0.0
|
||||
else:
|
||||
# extract the neural network for rsl-rl < 4.0.0
|
||||
if version.parse(installed_version) >= version.parse("2.3.0"):
|
||||
policy_nn = runner.alg.policy
|
||||
else:
|
||||
policy_nn = runner.alg.actor_critic
|
||||
|
||||
# extract the normalizer
|
||||
if hasattr(policy_nn, "actor_obs_normalizer"):
|
||||
normalizer = policy_nn.actor_obs_normalizer
|
||||
elif hasattr(policy_nn, "student_obs_normalizer"):
|
||||
normalizer = policy_nn.student_obs_normalizer
|
||||
else:
|
||||
normalizer = None
|
||||
|
||||
# export to JIT and ONNX
|
||||
export_policy_as_jit(policy_nn, normalizer=normalizer, path=export_model_dir, filename="policy.pt")
|
||||
export_policy_as_onnx(policy_nn, normalizer=normalizer, path=export_model_dir, filename="policy.onnx")
|
||||
|
||||
dt = env.unwrapped.step_dt
|
||||
|
||||
# reset environment
|
||||
obs = env.get_observations()
|
||||
timestep = 0
|
||||
# simulate environment
|
||||
try:
|
||||
while True:
|
||||
start_time = time.time()
|
||||
# run everything in inference mode
|
||||
with torch.inference_mode():
|
||||
# agent stepping
|
||||
actions = policy(obs)
|
||||
# env stepping
|
||||
obs, _, dones, _ = env.step(actions)
|
||||
# reset recurrent states for episodes that have terminated
|
||||
if version.parse(installed_version) >= version.parse("4.0.0"):
|
||||
policy.reset(dones)
|
||||
else:
|
||||
policy_nn.reset(dones)
|
||||
if args_cli.video:
|
||||
timestep += 1
|
||||
if timestep == args_cli.video_length:
|
||||
break
|
||||
|
||||
sleep_time = dt - (time.time() - start_time)
|
||||
if args_cli.real_time and sleep_time > 0:
|
||||
time.sleep(sleep_time)
|
||||
|
||||
# close the simulator
|
||||
env.close()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,234 @@
|
|||
# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md).
|
||||
# All rights reserved.
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
"""Script to play a checkpoint of an RL agent from RSL-RL."""
|
||||
|
||||
import argparse
|
||||
import contextlib
|
||||
import importlib.metadata as metadata
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
import gymnasium as gym
|
||||
import torch
|
||||
from packaging import version
|
||||
from rsl_rl.runners import DistillationRunner, OnPolicyRunner
|
||||
|
||||
from isaaclab.envs import DirectMARLEnvCfg, DirectRLEnvCfg, ManagerBasedRLEnvCfg
|
||||
from isaaclab.utils.assets import retrieve_file_path
|
||||
from isaaclab.utils.dict import print_dict
|
||||
from isaaclab.utils.string import list_intersection, string_to_callable
|
||||
|
||||
from isaaclab_rl.rsl_rl import (
|
||||
RslRlBaseRunnerCfg,
|
||||
RslRlVecEnvWrapper,
|
||||
export_policy_as_jit,
|
||||
export_policy_as_onnx,
|
||||
handle_deprecated_rsl_rl_cfg,
|
||||
)
|
||||
from isaaclab_rl.utils.pretrained_checkpoint import get_published_pretrained_checkpoint
|
||||
|
||||
import isaaclab_tasks # noqa: F401
|
||||
from isaaclab_tasks.utils import (
|
||||
add_launcher_args,
|
||||
get_checkpoint_path,
|
||||
launch_simulation,
|
||||
setup_preset_cli,
|
||||
)
|
||||
from isaaclab_tasks.utils.hydra import hydra_task_config
|
||||
|
||||
# local imports
|
||||
import cli_args # isort: skip
|
||||
|
||||
import go2Demo.tasks # noqa: F401
|
||||
with contextlib.suppress(ImportError):
|
||||
import isaaclab_tasks_experimental # noqa: F401
|
||||
|
||||
# -- argparse ----------------------------------------------------------------
|
||||
parser = argparse.ArgumentParser(description="Play a checkpoint of an RL agent from RSL-RL.")
|
||||
parser.add_argument("--video", action="store_true", default=False, help="Record videos during play.")
|
||||
parser.add_argument("--video_length", type=int, default=200, help="Length of the recorded video (in steps).")
|
||||
parser.add_argument(
|
||||
"--disable_fabric", action="store_true", default=False, help="Disable fabric and use USD I/O operations."
|
||||
)
|
||||
parser.add_argument("--num_envs", type=int, default=None, help="Number of environments to simulate.")
|
||||
parser.add_argument("--task", type=str, default=None, help="Name of the task.")
|
||||
parser.add_argument(
|
||||
"--agent", type=str, default="rsl_rl_cfg_entry_point", help="Name of the RL agent configuration entry point."
|
||||
)
|
||||
parser.add_argument("--seed", type=int, default=None, help="Seed used for the environment")
|
||||
parser.add_argument(
|
||||
"--use_pretrained_checkpoint",
|
||||
action="store_true",
|
||||
help="Use the pre-trained checkpoint from Nucleus.",
|
||||
)
|
||||
parser.add_argument("--real-time", action="store_true", default=False, help="Run in real-time, if possible.")
|
||||
parser.add_argument("--external_callback", default=None, help="Fully qualified path to an externally defined callback.")
|
||||
cli_args.add_rsl_rl_args(parser)
|
||||
add_launcher_args(parser)
|
||||
args_cli, remaining_args = setup_preset_cli(parser)
|
||||
|
||||
if args_cli.video:
|
||||
args_cli.enable_cameras = True
|
||||
|
||||
|
||||
# Call an external callback if requested. This gives opportunity to external code to register the environments
|
||||
# The function is expected to return a list of arguments that were not consumed by the callback.
|
||||
remaining_args_env_registration = None
|
||||
if args_cli.external_callback:
|
||||
external_callback_function = string_to_callable(args_cli.external_callback, separator=".")
|
||||
remaining_args_env_registration = external_callback_function()
|
||||
|
||||
# clear out sys.argv for Hydra
|
||||
# The remaining arguments are the arguments that were not consumed by both this scripts
|
||||
# argparser and (optionally) the external callback function.
|
||||
remaining_args = list_intersection(remaining_args, remaining_args_env_registration)
|
||||
sys.argv = [sys.argv[0]] + remaining_args
|
||||
|
||||
# Check for installed RSL-RL version
|
||||
installed_version = metadata.version("rsl-rl-lib")
|
||||
|
||||
|
||||
@hydra_task_config(args_cli.task, args_cli.agent)
|
||||
def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agent_cfg: RslRlBaseRunnerCfg):
|
||||
"""Play with RSL-RL agent."""
|
||||
with launch_simulation(env_cfg, args_cli):
|
||||
# grab task name for checkpoint path
|
||||
task_name = args_cli.task.split(":")[-1]
|
||||
train_task_name = task_name.replace("-Play", "")
|
||||
|
||||
# override configurations with non-hydra CLI arguments
|
||||
agent_cfg = cli_args.update_rsl_rl_cfg(agent_cfg, args_cli)
|
||||
env_cfg.scene.num_envs = args_cli.num_envs if args_cli.num_envs is not None else env_cfg.scene.num_envs
|
||||
|
||||
# handle deprecated configurations
|
||||
agent_cfg = handle_deprecated_rsl_rl_cfg(agent_cfg, installed_version)
|
||||
|
||||
# set the environment seed
|
||||
# note: certain randomizations occur in the environment initialization so we set the seed here
|
||||
env_cfg.seed = agent_cfg.seed
|
||||
env_cfg.sim.device = args_cli.device if args_cli.device is not None else env_cfg.sim.device
|
||||
|
||||
# specify directory for logging experiments
|
||||
log_root_path = os.path.join("logs", "rsl_rl", agent_cfg.experiment_name)
|
||||
log_root_path = os.path.abspath(log_root_path)
|
||||
print(f"[INFO] Loading experiment from directory: {log_root_path}")
|
||||
if args_cli.use_pretrained_checkpoint:
|
||||
resume_path = get_published_pretrained_checkpoint("rsl_rl", train_task_name)
|
||||
if not resume_path:
|
||||
print("[INFO] Unfortunately a pre-trained checkpoint is currently unavailable for this task.")
|
||||
return
|
||||
elif args_cli.checkpoint:
|
||||
resume_path = retrieve_file_path(args_cli.checkpoint)
|
||||
else:
|
||||
resume_path = get_checkpoint_path(log_root_path, agent_cfg.load_run, agent_cfg.load_checkpoint)
|
||||
|
||||
log_dir = os.path.dirname(resume_path)
|
||||
|
||||
# set the log directory for the environment
|
||||
env_cfg.log_dir = log_dir
|
||||
|
||||
# create isaac environment
|
||||
env = gym.make(args_cli.task, cfg=env_cfg, render_mode="rgb_array" if args_cli.video else None)
|
||||
|
||||
# convert to single-agent instance if required by the RL algorithm
|
||||
if isinstance(env.unwrapped.cfg, DirectMARLEnvCfg):
|
||||
from isaaclab.envs import multi_agent_to_single_agent
|
||||
|
||||
env = multi_agent_to_single_agent(env)
|
||||
|
||||
# wrap for video recording
|
||||
if args_cli.video:
|
||||
video_kwargs = {
|
||||
"video_folder": os.path.join(log_dir, "videos", "play"),
|
||||
"step_trigger": lambda step: step == 0,
|
||||
"video_length": args_cli.video_length,
|
||||
"disable_logger": True,
|
||||
}
|
||||
print("[INFO] Recording videos during play.")
|
||||
print_dict(video_kwargs, nesting=4)
|
||||
env = gym.wrappers.RecordVideo(env, **video_kwargs)
|
||||
|
||||
# wrap around environment for rsl-rl
|
||||
env = RslRlVecEnvWrapper(env, clip_actions=agent_cfg.clip_actions)
|
||||
|
||||
print(f"[INFO]: Loading model checkpoint from: {resume_path}")
|
||||
# load previously trained model
|
||||
if agent_cfg.class_name == "OnPolicyRunner":
|
||||
runner = OnPolicyRunner(env, agent_cfg.to_dict(), log_dir=None, device=agent_cfg.device)
|
||||
elif agent_cfg.class_name == "DistillationRunner":
|
||||
runner = DistillationRunner(env, agent_cfg.to_dict(), log_dir=None, device=agent_cfg.device)
|
||||
else:
|
||||
raise ValueError(f"Unsupported runner class: {agent_cfg.class_name}")
|
||||
runner.load(resume_path)
|
||||
|
||||
# obtain the trained policy for inference
|
||||
policy = runner.get_inference_policy(device=env.unwrapped.device)
|
||||
|
||||
# export the trained policy to JIT and ONNX formats
|
||||
export_model_dir = os.path.join(os.path.dirname(resume_path), "exported")
|
||||
|
||||
if version.parse(installed_version) >= version.parse("4.0.0"):
|
||||
# use the new export functions for rsl-rl >= 4.0.0
|
||||
runner.export_policy_to_jit(path=export_model_dir, filename="policy.pt")
|
||||
runner.export_policy_to_onnx(path=export_model_dir, filename="policy.onnx")
|
||||
policy_nn = None # Not needed for rsl-rl >= 4.0.0
|
||||
else:
|
||||
# extract the neural network for rsl-rl < 4.0.0
|
||||
if version.parse(installed_version) >= version.parse("2.3.0"):
|
||||
policy_nn = runner.alg.policy
|
||||
else:
|
||||
policy_nn = runner.alg.actor_critic
|
||||
|
||||
# extract the normalizer
|
||||
if hasattr(policy_nn, "actor_obs_normalizer"):
|
||||
normalizer = policy_nn.actor_obs_normalizer
|
||||
elif hasattr(policy_nn, "student_obs_normalizer"):
|
||||
normalizer = policy_nn.student_obs_normalizer
|
||||
else:
|
||||
normalizer = None
|
||||
|
||||
# export to JIT and ONNX
|
||||
export_policy_as_jit(policy_nn, normalizer=normalizer, path=export_model_dir, filename="policy.pt")
|
||||
export_policy_as_onnx(policy_nn, normalizer=normalizer, path=export_model_dir, filename="policy.onnx")
|
||||
|
||||
dt = env.unwrapped.step_dt
|
||||
|
||||
# reset environment
|
||||
obs = env.get_observations()
|
||||
timestep = 0
|
||||
# simulate environment
|
||||
try:
|
||||
while True:
|
||||
start_time = time.time()
|
||||
# run everything in inference mode
|
||||
with torch.inference_mode():
|
||||
# agent stepping
|
||||
actions = policy(obs)
|
||||
# env stepping
|
||||
obs, _, dones, _ = env.step(actions)
|
||||
# reset recurrent states for episodes that have terminated
|
||||
if version.parse(installed_version) >= version.parse("4.0.0"):
|
||||
policy.reset(dones)
|
||||
else:
|
||||
policy_nn.reset(dones)
|
||||
if args_cli.video:
|
||||
timestep += 1
|
||||
if timestep == args_cli.video_length:
|
||||
break
|
||||
|
||||
sleep_time = dt - (time.time() - start_time)
|
||||
if args_cli.real_time and sleep_time > 0:
|
||||
time.sleep(sleep_time)
|
||||
|
||||
# close the simulator
|
||||
env.close()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,183 @@
|
|||
# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md).
|
||||
# All rights reserved.
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
"""RSL-RL training logic for the unified reinforcement learning entrypoint."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import contextlib
|
||||
import importlib.metadata as metadata
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
import time
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from common import (
|
||||
add_common_train_args,
|
||||
add_isaaclab_launcher_args,
|
||||
apply_env_overrides,
|
||||
configure_io_descriptors,
|
||||
create_isaaclab_env,
|
||||
dump_train_configs,
|
||||
enable_cameras_for_video,
|
||||
import_local_module,
|
||||
set_hydra_args,
|
||||
validate_distributed_device,
|
||||
wrap_record_video,
|
||||
)
|
||||
from packaging import version
|
||||
|
||||
import isaaclab_tasks # noqa: F401
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
RSL_RL_VERSION = "5.0.1"
|
||||
RL_ROOT = Path(__file__).resolve().parents[1]
|
||||
CLI_ARGS = import_local_module("isaaclab_rsl_rl_cli_args", RL_ROOT / "rsl_rl" / "cli_args.py")
|
||||
|
||||
import go2Demo.tasks # noqa: F401
|
||||
with contextlib.suppress(ImportError):
|
||||
import isaaclab_tasks_experimental # noqa: F401
|
||||
|
||||
|
||||
def _check_rsl_rl_version() -> str:
|
||||
"""Check that the installed RSL-RL version is supported."""
|
||||
installed_version = metadata.version("rsl-rl-lib")
|
||||
if version.parse(installed_version) < version.parse(RSL_RL_VERSION):
|
||||
if platform.system() == "Windows":
|
||||
cmd = [r".\isaaclab.bat", "-p", "-m", "pip", "install", f"rsl-rl-lib=={RSL_RL_VERSION}"]
|
||||
else:
|
||||
cmd = ["./isaaclab.sh", "-p", "-m", "pip", "install", f"rsl-rl-lib=={RSL_RL_VERSION}"]
|
||||
print(
|
||||
f"Please install the correct version of RSL-RL.\nExisting version is: '{installed_version}'"
|
||||
f" and required version is: '{RSL_RL_VERSION}'.\nTo install the correct version, run:"
|
||||
f"\n\n\t{' '.join(cmd)}\n"
|
||||
)
|
||||
raise SystemExit(1)
|
||||
return installed_version
|
||||
|
||||
|
||||
def _parse_args(argv: list[str]) -> argparse.Namespace:
|
||||
"""Parse RSL-RL training arguments."""
|
||||
from isaaclab.utils.string import list_intersection, string_to_callable
|
||||
|
||||
from isaaclab_tasks.utils import setup_preset_cli
|
||||
|
||||
parser = argparse.ArgumentParser(description="Train an RL agent with RSL-RL.")
|
||||
add_common_train_args(
|
||||
parser,
|
||||
agent_default="rsl_rl_cfg_entry_point",
|
||||
agent_help="Name of the RL agent configuration entry point.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--external_callback",
|
||||
default=None,
|
||||
help="Fully qualified path to an externally defined callback.",
|
||||
)
|
||||
CLI_ARGS.add_rsl_rl_args(parser)
|
||||
add_isaaclab_launcher_args(parser)
|
||||
# setup_preset_cli registers preset-selection help text + runs parse_known_args
|
||||
args_cli, remaining_args = setup_preset_cli(parser, argv)
|
||||
enable_cameras_for_video(args_cli)
|
||||
|
||||
remaining_args_env_registration = None
|
||||
if args_cli.external_callback:
|
||||
external_callback_function = string_to_callable(args_cli.external_callback, separator=".")
|
||||
remaining_args_env_registration = external_callback_function()
|
||||
|
||||
# physics=/renderer=/presets= tokens pass through the remainder for hydra to parse later
|
||||
set_hydra_args(list_intersection(remaining_args, remaining_args_env_registration))
|
||||
return args_cli
|
||||
|
||||
|
||||
def run(argv: list[str]) -> None:
|
||||
"""Train an RSL-RL agent."""
|
||||
import torch
|
||||
from rsl_rl.runners import DistillationRunner, OnPolicyRunner
|
||||
|
||||
from isaaclab.envs import DirectMARLEnvCfg
|
||||
|
||||
from isaaclab_rl.rsl_rl import RslRlVecEnvWrapper, handle_deprecated_rsl_rl_cfg
|
||||
|
||||
from isaaclab_tasks.utils import get_checkpoint_path, launch_simulation, resolve_task_config
|
||||
|
||||
torch.backends.cuda.matmul.allow_tf32 = True
|
||||
torch.backends.cudnn.allow_tf32 = True
|
||||
torch.backends.cudnn.deterministic = False
|
||||
torch.backends.cudnn.benchmark = False
|
||||
|
||||
args_cli = _parse_args(argv)
|
||||
installed_version = _check_rsl_rl_version()
|
||||
env_cfg, agent_cfg = resolve_task_config(args_cli.task, args_cli.agent)
|
||||
|
||||
with launch_simulation(env_cfg, args_cli):
|
||||
agent_cfg = CLI_ARGS.update_rsl_rl_cfg(agent_cfg, args_cli)
|
||||
apply_env_overrides(args_cli, env_cfg)
|
||||
agent_cfg.max_iterations = (
|
||||
args_cli.max_iterations if args_cli.max_iterations is not None else agent_cfg.max_iterations
|
||||
)
|
||||
|
||||
agent_cfg = handle_deprecated_rsl_rl_cfg(agent_cfg, installed_version)
|
||||
|
||||
env_cfg.seed = agent_cfg.seed
|
||||
validate_distributed_device(args_cli)
|
||||
|
||||
if args_cli.distributed:
|
||||
global_rank = int(os.getenv("RANK", "0"))
|
||||
agent_cfg.device = env_cfg.sim.device
|
||||
|
||||
seed = agent_cfg.seed + global_rank
|
||||
env_cfg.seed = seed
|
||||
agent_cfg.seed = seed
|
||||
|
||||
log_root_path = os.path.abspath(os.path.join("logs", "rsl_rl", agent_cfg.experiment_name))
|
||||
print(f"[INFO] Logging experiment in directory: {log_root_path}")
|
||||
log_dir = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
||||
print(f"Exact experiment name requested from command line: {log_dir}")
|
||||
if agent_cfg.run_name:
|
||||
log_dir += f"_{agent_cfg.run_name}"
|
||||
log_dir = os.path.join(log_root_path, log_dir)
|
||||
|
||||
configure_io_descriptors(env_cfg, args_cli, logger)
|
||||
env_cfg.log_dir = log_dir
|
||||
|
||||
env = create_isaaclab_env(
|
||||
args_cli.task,
|
||||
env_cfg,
|
||||
args_cli,
|
||||
convert_marl_to_single_agent=isinstance(env_cfg, DirectMARLEnvCfg),
|
||||
)
|
||||
|
||||
if agent_cfg.resume or agent_cfg.algorithm.class_name == "Distillation":
|
||||
resume_path = get_checkpoint_path(log_root_path, agent_cfg.load_run, agent_cfg.load_checkpoint)
|
||||
|
||||
env = wrap_record_video(env, log_dir, args_cli)
|
||||
|
||||
start_time = time.time()
|
||||
env = RslRlVecEnvWrapper(env, clip_actions=agent_cfg.clip_actions)
|
||||
|
||||
if agent_cfg.class_name == "OnPolicyRunner":
|
||||
runner = OnPolicyRunner(env, agent_cfg.to_dict(), log_dir=log_dir, device=agent_cfg.device)
|
||||
elif agent_cfg.class_name == "DistillationRunner":
|
||||
runner = DistillationRunner(env, agent_cfg.to_dict(), log_dir=log_dir, device=agent_cfg.device)
|
||||
else:
|
||||
raise ValueError(f"Unsupported runner class: {agent_cfg.class_name}")
|
||||
|
||||
runner.add_git_repo_to_log(__file__)
|
||||
if agent_cfg.resume or agent_cfg.algorithm.class_name == "Distillation":
|
||||
print(f"[INFO]: Loading model checkpoint from: {resume_path}")
|
||||
runner.load(resume_path)
|
||||
|
||||
dump_train_configs(log_dir, env_cfg, agent_cfg)
|
||||
|
||||
try:
|
||||
runner.learn(num_learning_iterations=agent_cfg.max_iterations, init_at_random_ep_len=True)
|
||||
print(f"Training time: {round(time.time() - start_time, 2)} seconds")
|
||||
env.close()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md).
|
||||
# All rights reserved.
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
"""Script to run an environment with zero action agent."""
|
||||
|
||||
import argparse
|
||||
import contextlib
|
||||
import sys
|
||||
|
||||
import gymnasium as gym
|
||||
import torch
|
||||
|
||||
import isaaclab_tasks # noqa: F401
|
||||
|
||||
with contextlib.suppress(ImportError):
|
||||
import isaaclab_tasks_experimental # noqa: F401
|
||||
from isaaclab_tasks.utils import (
|
||||
add_launcher_args,
|
||||
launch_simulation,
|
||||
resolve_task_config,
|
||||
setup_preset_cli,
|
||||
)
|
||||
|
||||
# add argparse arguments
|
||||
parser = argparse.ArgumentParser(description="Zero agent for Isaac Lab environments.")
|
||||
parser.add_argument(
|
||||
"--disable_fabric", action="store_true", default=False, help="Disable fabric and use USD I/O operations."
|
||||
)
|
||||
parser.add_argument("--num_envs", type=int, default=None, help="Number of environments to simulate.")
|
||||
parser.add_argument("--task", type=str, default=None, help="Name of the task.")
|
||||
# append AppLauncher cli args
|
||||
add_launcher_args(parser)
|
||||
# simple agents should open Kit visualizer by default
|
||||
parser.set_defaults(visualizer=["kit"])
|
||||
args_cli, hydra_args = setup_preset_cli(parser)
|
||||
sys.argv = [sys.argv[0]] + hydra_args
|
||||
|
||||
import go2Demo.tasks # noqa: F401
|
||||
MAX_STEPS = 100
|
||||
|
||||
|
||||
def main():
|
||||
"""Zero actions agent with Isaac Lab environment."""
|
||||
|
||||
torch.manual_seed(42)
|
||||
|
||||
# parse configuration via Hydra (supports preset selection, e.g. env.sim.physics=newton_mjwarp)
|
||||
env_cfg, _ = resolve_task_config(args_cli.task, "")
|
||||
|
||||
with launch_simulation(env_cfg, args_cli):
|
||||
# override with CLI arguments
|
||||
env_cfg.scene.num_envs = args_cli.num_envs if args_cli.num_envs is not None else env_cfg.scene.num_envs
|
||||
env_cfg.sim.device = args_cli.device if args_cli.device is not None else env_cfg.sim.device
|
||||
if args_cli.disable_fabric:
|
||||
env_cfg.sim.use_fabric = False
|
||||
|
||||
# create environment
|
||||
env = gym.make(args_cli.task, cfg=env_cfg)
|
||||
|
||||
# print info (this is vectorized environment)
|
||||
print(f"[INFO]: Gym observation space: {env.observation_space}")
|
||||
print(f"[INFO]: Gym action space: {env.action_space}")
|
||||
# reset environment
|
||||
env.reset()
|
||||
# simulate environment
|
||||
# keep running while any visualizer is open, otherwise fall back to MAX_STEPS
|
||||
sim = env.unwrapped.sim
|
||||
actions = torch.zeros(env.action_space.shape, device=env.unwrapped.device)
|
||||
while True:
|
||||
if sim.visualizers:
|
||||
# visualizer mode: run until the visualizer window is closed
|
||||
if not any(v.is_running() and not v.is_closed for v in sim.visualizers):
|
||||
break
|
||||
# run everything in inference mode
|
||||
with torch.inference_mode():
|
||||
# apply actions
|
||||
env.step(actions)
|
||||
# close the simulator
|
||||
env.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# run the main function
|
||||
main()
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
[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 = "go2Demo"
|
||||
|
||||
# UI extension module: Kit imports this submodule directly and scans it for ``omni.ext.IExt``
|
||||
# subclasses. Kept separate from the package root so ``import go2Demo`` stays omni-free headless.
|
||||
[[python.module]]
|
||||
name = "go2Demo.ui_extension_example"
|
||||
|
||||
[isaac_lab_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"
|
||||
# TODO: Uncomment and list install_requires dependency names that should be upgraded
|
||||
# after this extension is installed with ./isaaclab.sh --install.
|
||||
# List package names only; version ranges, extras, and platform markers
|
||||
# come from this extension's setup.py metadata.
|
||||
# pip_upgrade_dependencies = ["example_package"]
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
Changelog
|
||||
---------
|
||||
|
||||
0.1.0 (2026-08-08)
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Added
|
||||
^^^^^
|
||||
|
||||
* Created an initial template for building an extension or project based on Isaac Lab
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md).
|
||||
# All rights reserved.
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
from isaaclab.utils.configclass import configclass
|
||||
|
||||
from isaaclab_rl.rsl_rl import RslRlMLPModelCfg, RslRlOnPolicyRunnerCfg, RslRlPpoAlgorithmCfg
|
||||
|
||||
|
||||
@configclass
|
||||
class PPORunnerCfg(RslRlOnPolicyRunnerCfg):
|
||||
|
||||
"""
|
||||
PPO算法运行配置类,继承自RslRlOnPolicyRunnerCfg。
|
||||
定义了PPO算法训练过程中的各种参数设置。
|
||||
"""
|
||||
num_steps_per_env = 16 # 每个环境执行的步数
|
||||
max_iterations = 150 # 最大迭代次数
|
||||
save_interval = 50 # 模型保存间隔
|
||||
experiment_name = "cartpole_direct" # 实验名称,用于标识当前实验
|
||||
# Actor网络配置
|
||||
actor = RslRlMLPModelCfg(
|
||||
hidden_dims=[32, 32], # 隐藏层维度
|
||||
activation="elu", # 激活函数类型
|
||||
obs_normalization=False, # 是否进行观测值归一化
|
||||
distribution_cfg=RslRlMLPModelCfg.GaussianDistributionCfg(init_std=1.0), # 动作分布配置
|
||||
)
|
||||
# Critic网络配置
|
||||
critic = RslRlMLPModelCfg(
|
||||
hidden_dims=[32, 32], # 隐藏层维度
|
||||
activation="elu", # 激活函数类型
|
||||
obs_normalization=False, # 是否进行观测值归一化
|
||||
)
|
||||
# PPO算法配置
|
||||
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, # GAE(lambda)参数
|
||||
desired_kl=0.01, # 期望的KL散度
|
||||
max_grad_norm=1.0, # 最大梯度范数
|
||||
)
|
||||
|
|
@ -0,0 +1,195 @@
|
|||
# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md).
|
||||
# All rights reserved.
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
import 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.configclass import configclass
|
||||
|
||||
from . import mdp
|
||||
|
||||
##
|
||||
# Pre-defined configs
|
||||
##
|
||||
|
||||
from isaaclab_assets.robots.cartpole import CARTPOLE_CFG # isort:skip
|
||||
|
||||
|
||||
##
|
||||
# Scene definition
|
||||
##
|
||||
|
||||
|
||||
@configclass
|
||||
class Go2demoSceneCfg(InteractiveSceneCfg):
|
||||
"""Configuration for a cart-pole scene.""" # 多行注释:这是一个用于配置-cart-pole场景的类
|
||||
|
||||
# ground plane # 单行注释:地面平面配置
|
||||
ground = AssetBaseCfg(
|
||||
prim_path="/World/ground", # 单行注释:地面的基础路径
|
||||
spawn=sim_utils.GroundPlaneCfg(size=(100.0, 100.0)), # 单行注释:生成一个大小为100x100的地面平面
|
||||
)
|
||||
|
||||
# robot # 单行注释:机器人配置
|
||||
robot: ArticulationCfg = CARTPOLE_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot") # 单行注释:使用CARTPOLE_CFG配置并替换机器人路径
|
||||
|
||||
# lights # 单行注释:灯光配置
|
||||
dome_light = AssetBaseCfg(
|
||||
prim_path="/World/DomeLight", # 单行注释:穹顶灯光的基础路径
|
||||
spawn=sim_utils.DomeLightCfg(color=(0.9, 0.9, 0.9), intensity=500.0), # 单行注释:生成一个颜色为(0.9, 0.9, 0.9)、强度为500.0的穹顶灯光
|
||||
)
|
||||
|
||||
|
||||
##
|
||||
# MDP settings
|
||||
##
|
||||
|
||||
|
||||
@configclass
|
||||
class ActionsCfg:
|
||||
|
||||
joint_effort = mdp.JointEffortActionCfg(asset_name="robot", joint_names=["slider_to_cart"], scale=100.0)
|
||||
|
||||
|
||||
@configclass
|
||||
class ObservationsCfg:
|
||||
|
||||
"""观察配置类,用于定义和管理观察相关的配置项"""
|
||||
@configclass
|
||||
class PolicyCfg(ObsGroup):
|
||||
|
||||
"""策略配置类,继承自ObsGroup,用于定义策略相关的观察项"""
|
||||
# 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.enable_corruption = False # 禁用数据损坏
|
||||
self.concatenate_terms = True # 启用观察项连接
|
||||
|
||||
policy: PolicyCfg = PolicyCfg() # 实例化策略配置类作为观察组
|
||||
|
||||
|
||||
@configclass
|
||||
class EventCfg:
|
||||
|
||||
"""
|
||||
事件配置类,用于定义各种事件及其参数配置。
|
||||
包含两个事件:重置小车位置和重置摆杆位置。
|
||||
"""
|
||||
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), # 位置范围,限制小车的位置在-1.0到1.0之间
|
||||
"velocity_range": (-0.5, 0.5), # 速度范围,限制小车的速度在-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), # 位置范围,限制摆杆的位置在-π/4到π/4之间
|
||||
"velocity_range": (-0.25 * math.pi, 0.25 * math.pi), # 速度范围,限制摆杆的速度在-π/4到π/4之间
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@configclass
|
||||
class RewardsCfg:
|
||||
|
||||
"""
|
||||
奖励配置类,用于定义各种奖励项及其权重
|
||||
"""
|
||||
# 存活奖励项:如果智能体存活,则获得1.0的奖励
|
||||
alive = RewTerm(func=mdp.is_alive, weight=1.0)
|
||||
# 终止惩罚项:如果智能体终止,则获得-2.0的惩罚
|
||||
terminating = RewTerm(func=mdp.is_terminated, weight=-2.0)
|
||||
# 杆位置奖励项:鼓励杆保持在目标位置(0.0),偏离目标位置会受到惩罚
|
||||
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},
|
||||
)
|
||||
# 小车速度奖励项:限制小车的速度,速度过快会受到惩罚
|
||||
cart_vel = RewTerm(
|
||||
func=mdp.joint_vel_l1,
|
||||
weight=-0.01,
|
||||
params={"asset_cfg": SceneEntityCfg("robot", joint_names=["slider_to_cart"])},
|
||||
)
|
||||
# 杆速度奖励项:限制杆的速度,速度过快会受到惩罚
|
||||
pole_vel = RewTerm(
|
||||
func=mdp.joint_vel_l1,
|
||||
weight=-0.005,
|
||||
params={"asset_cfg": SceneEntityCfg("robot", joint_names=["cart_to_pole"])},
|
||||
)
|
||||
|
||||
|
||||
@configclass
|
||||
class TerminationsCfg:
|
||||
|
||||
|
||||
time_out = DoneTerm(func=mdp.time_out, time_out=True)
|
||||
|
||||
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)},
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@configclass
|
||||
class Go2demoEnvCfg(ManagerBasedRLEnvCfg):
|
||||
|
||||
|
||||
"""
|
||||
Go2demo环境配置类,继承自ManagerBasedRLEnvCfg
|
||||
用于配置和管理Go2机器人在模拟环境中的各种参数
|
||||
"""
|
||||
scene: Go2demoSceneCfg = Go2demoSceneCfg(num_envs=4096, env_spacing=4.0) # 场景配置,设置环境数量和间距
|
||||
|
||||
observations: ObservationsCfg = ObservationsCfg() # 观察配置,定义智能体可以观察到的状态信息
|
||||
actions: ActionsCfg = ActionsCfg() # 动作配置,定义智能体可以执行的动作
|
||||
events: EventCfg = EventCfg() # 事件配置,定义环境中的各种事件
|
||||
|
||||
rewards: RewardsCfg = RewardsCfg() # 奖励配置,定义智能体获得奖励的规则
|
||||
terminations: TerminationsCfg = TerminationsCfg() # 终止条件配置,定义回合结束的条件
|
||||
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
|
||||
|
||||
"""
|
||||
初始化方法,在对象创建后自动调用
|
||||
用于设置和配置模拟环境的各种参数
|
||||
"""
|
||||
self.decimation = 2 # 降采样率,控制模拟的频率
|
||||
self.episode_length_s = 5 # 每个回合的持续时间(秒)
|
||||
|
||||
self.viewer.eye = (8.0, 0.0, 5.0) # 设置观察者的位置坐标(x, y, z)
|
||||
|
||||
self.sim.dt = 1 / 120 # 模拟时间步长(秒)
|
||||
self.sim.render_interval = self.decimation # 渲染间隔,基于降采样率设置
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md).
|
||||
# All rights reserved.
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import torch
|
||||
|
||||
from isaaclab.managers import SceneEntityCfg
|
||||
from isaaclab.utils.math import wrap_to_pi
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from isaaclab.assets import Articulation
|
||||
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)
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md).
|
||||
# All rights reserved.
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
import omni.ext
|
||||
import omni.ui # used by ExampleExtension.on_startup
|
||||
|
||||
|
||||
# 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("[go2Demo] 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("[go2Demo] 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("[go2Demo] shutdown")
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
[build-system]
|
||||
requires = ["setuptools<82.0.0", "wheel", "toml"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md).
|
||||
# All rights reserved.
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
"""Installation script for the 'go2Demo' 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="go2Demo",
|
||||
packages=["go2Demo"],
|
||||
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="Apache-2.0",
|
||||
include_package_data=True,
|
||||
python_requires=">=3.12",
|
||||
classifiers=[
|
||||
"Natural Language :: English",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Isaac Sim :: 6.0.0",
|
||||
],
|
||||
zip_safe=False,
|
||||
)
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
#!/bin/bash
|
||||
# ============================================================
|
||||
# 脚本名称: git-force-sync.sh
|
||||
# 功能描述: 强制让本地分支与远程仓库完全一致(远程覆盖本地)
|
||||
# 警告提示: 会永久删除本地所有未提交的修改、未推送的提交和未跟踪的文件
|
||||
# ============================================================
|
||||
|
||||
set -e # 遇到错误立即退出
|
||||
|
||||
# ---------- 颜色定义(提升可读性)----------
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# ---------- 1. 检查是否在 Git 仓库中 ----------
|
||||
if ! git rev-parse --is-inside-work-tree > /dev/null 2>&1; then
|
||||
echo -e "${RED}错误:当前目录不是 Git 仓库${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ---------- 2. 获取当前分支名 ----------
|
||||
CURRENT_BRANCH=$(git branch --show-current)
|
||||
if [ -z "$CURRENT_BRANCH" ]; then
|
||||
echo -e "${RED}错误:当前处于 HEAD 分离状态(detached HEAD),请先切换到具体分支。${NC}"
|
||||
exit 1
|
||||
fi
|
||||
echo -e "${BLUE}当前分支:${CURRENT_BRANCH}${NC}"
|
||||
|
||||
# ---------- 3. 检查本地是否有未提交的更改 ----------
|
||||
if ! git diff --quiet || ! git diff --cached --quiet; then
|
||||
echo -e "${YELLOW}⚠️ 检测到本地存在未提交的更改(包括已暂存和未暂存)。${NC}"
|
||||
echo -e "${RED}这些更改将在同步后永久丢失!${NC}"
|
||||
read -p "确定要继续吗?请输入 'yes' 以确认: " CONFIRM
|
||||
if [ "$CONFIRM" != "yes" ]; then
|
||||
echo -e "${GREEN}操作已取消。${NC}"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# ---------- 4. 检查是否有未跟踪的文件(可选清理提示) ----------
|
||||
UNTRACKED=$(git ls-files --others --exclude-standard | head -n 1)
|
||||
if [ -n "$UNTRACKED" ]; then
|
||||
echo -e "${YELLOW}⚠️ 检测到本地存在未跟踪的文件(如编译产物、日志等)。${NC}"
|
||||
read -p "是否同步删除这些未跟踪文件?(y/N) " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
DO_CLEAN=true
|
||||
else
|
||||
DO_CLEAN=false
|
||||
fi
|
||||
else
|
||||
DO_CLEAN=false
|
||||
fi
|
||||
|
||||
# ---------- 5. 拉取远程最新引用 ----------
|
||||
echo -e "${BLUE}正在从远程仓库获取最新引用...${NC}"
|
||||
git fetch origin
|
||||
|
||||
# ---------- 6. 验证远程分支是否存在 ----------
|
||||
if ! git rev-parse "origin/$CURRENT_BRANCH" > /dev/null 2>&1; then
|
||||
echo -e "${RED}错误:远程不存在分支 origin/$CURRENT_BRANCH,请检查分支名。${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ---------- 7. 执行强制覆盖(硬重置) ----------
|
||||
echo -e "${BLUE}正在将本地重置为 origin/$CURRENT_BRANCH ...${NC}"
|
||||
git reset --hard "origin/$CURRENT_BRANCH"
|
||||
|
||||
# ---------- 8. 清理未跟踪文件 ----------
|
||||
if [ "$DO_CLEAN" = true ]; then
|
||||
echo -e "${BLUE}正在删除所有未跟踪的文件和目录...${NC}"
|
||||
git clean -fd
|
||||
else
|
||||
echo -e "${YELLOW}已跳过删除未跟踪文件。${NC}"
|
||||
fi
|
||||
|
||||
# ---------- 9. 完成 ----------
|
||||
echo -e "${GREEN}✅ 同步完成!本地分支 '${CURRENT_BRANCH}' 已与远程 'origin/${CURRENT_BRANCH}' 完全一致。${NC}"
|
||||
Loading…
Reference in New Issue