commit 4a8b860f8750c45161bcfe80f0682442ce43e8c2 Author: zhenai Date: Sun Aug 9 05:43:03 2026 +0800 first commit diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..b718bd1 --- /dev/null +++ b/.dockerignore @@ -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/ diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..99e3579 --- /dev/null +++ b/.gitattributes @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..60989ad --- /dev/null +++ b/.gitignore @@ -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/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..5c2a029 --- /dev/null +++ b/.pre-commit-config.yaml @@ -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 diff --git a/.vscode/.gitignore b/.vscode/.gitignore new file mode 100644 index 0000000..10b0af3 --- /dev/null +++ b/.vscode/.gitignore @@ -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 diff --git a/.vscode/tools/setup_vscode.py b/.vscode/tools/setup_vscode.py new file mode 100644 index 0000000..8d29daf --- /dev/null +++ b/.vscode/tools/setup_vscode.py @@ -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() diff --git a/README.md b/README.md new file mode 100644 index 0000000..afabd26 --- /dev/null +++ b/README.md @@ -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//train.py --task= + ``` + + - 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= + ``` + - 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= + ``` + +### 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": [ + "/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 +"/extscache/omni.anim.*" // Animation packages +"/extscache/omni.kit.*" // Kit UI tools +"/extscache/omni.graph.*" // Graph UI tools +"/extscache/omni.services.*" // Services tools +... +``` \ No newline at end of file diff --git a/git-force-push.bat b/git-force-push.bat new file mode 100644 index 0000000..5c08f13 --- /dev/null +++ b/git-force-push.bat @@ -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 diff --git a/git-force-push.sh b/git-force-push.sh new file mode 100755 index 0000000..73d3c4f --- /dev/null +++ b/git-force-push.sh @@ -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}" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..a0797f7 --- /dev/null +++ b/pyproject.toml @@ -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" diff --git a/scripts/list_envs.py b/scripts/list_envs.py new file mode 100644 index 0000000..353ffef --- /dev/null +++ b/scripts/list_envs.py @@ -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() diff --git a/scripts/random_agent.py b/scripts/random_agent.py new file mode 100644 index 0000000..06d9c95 --- /dev/null +++ b/scripts/random_agent.py @@ -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() diff --git a/scripts/rsl_rl/cli_args.py b/scripts/rsl_rl/cli_args.py new file mode 100644 index 0000000..10edbe2 --- /dev/null +++ b/scripts/rsl_rl/cli_args.py @@ -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 diff --git a/scripts/rsl_rl/play.py b/scripts/rsl_rl/play.py new file mode 100644 index 0000000..3564e66 --- /dev/null +++ b/scripts/rsl_rl/play.py @@ -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 ` 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() diff --git a/scripts/rsl_rl/play_rsl_rl.py b/scripts/rsl_rl/play_rsl_rl.py new file mode 100644 index 0000000..c9a588d --- /dev/null +++ b/scripts/rsl_rl/play_rsl_rl.py @@ -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() diff --git a/scripts/rsl_rl/train_rsl_rl.py b/scripts/rsl_rl/train_rsl_rl.py new file mode 100644 index 0000000..c080893 --- /dev/null +++ b/scripts/rsl_rl/train_rsl_rl.py @@ -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 diff --git a/scripts/zero_agent.py b/scripts/zero_agent.py new file mode 100644 index 0000000..96922aa --- /dev/null +++ b/scripts/zero_agent.py @@ -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() diff --git a/source/go2Demo/config/extension.toml b/source/go2Demo/config/extension.toml new file mode 100644 index 0000000..8440c0c --- /dev/null +++ b/source/go2Demo/config/extension.toml @@ -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"] \ No newline at end of file diff --git a/source/go2Demo/docs/CHANGELOG.rst b/source/go2Demo/docs/CHANGELOG.rst new file mode 100644 index 0000000..32dd5df --- /dev/null +++ b/source/go2Demo/docs/CHANGELOG.rst @@ -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 \ No newline at end of file diff --git a/source/go2Demo/go2Demo/tasks/manager_based/go2demo/agents/rsl_rl_ppo_cfg.py b/source/go2Demo/go2Demo/tasks/manager_based/go2demo/agents/rsl_rl_ppo_cfg.py new file mode 100644 index 0000000..74f66a9 --- /dev/null +++ b/source/go2Demo/go2Demo/tasks/manager_based/go2demo/agents/rsl_rl_ppo_cfg.py @@ -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, # 最大梯度范数 + ) \ No newline at end of file diff --git a/source/go2Demo/go2Demo/tasks/manager_based/go2demo/go2demo_env_cfg.py b/source/go2Demo/go2Demo/tasks/manager_based/go2demo/go2demo_env_cfg.py new file mode 100644 index 0000000..26bfe09 --- /dev/null +++ b/source/go2Demo/go2Demo/tasks/manager_based/go2demo/go2demo_env_cfg.py @@ -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 # 渲染间隔,基于降采样率设置 \ No newline at end of file diff --git a/source/go2Demo/go2Demo/tasks/manager_based/go2demo/mdp/rewards.py b/source/go2Demo/go2Demo/tasks/manager_based/go2demo/mdp/rewards.py new file mode 100644 index 0000000..bf47205 --- /dev/null +++ b/source/go2Demo/go2Demo/tasks/manager_based/go2demo/mdp/rewards.py @@ -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) diff --git a/source/go2Demo/go2Demo/ui_extension_example.py b/source/go2Demo/go2Demo/ui_extension_example.py new file mode 100644 index 0000000..a77e040 --- /dev/null +++ b/source/go2Demo/go2Demo/ui_extension_example.py @@ -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") \ No newline at end of file diff --git a/source/go2Demo/pyproject.toml b/source/go2Demo/pyproject.toml new file mode 100644 index 0000000..31dce8d --- /dev/null +++ b/source/go2Demo/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools<82.0.0", "wheel", "toml"] +build-backend = "setuptools.build_meta" diff --git a/source/go2Demo/setup.py b/source/go2Demo/setup.py new file mode 100644 index 0000000..0cb6e8d --- /dev/null +++ b/source/go2Demo/setup.py @@ -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, +) \ No newline at end of file diff --git a/sync.sh b/sync.sh new file mode 100755 index 0000000..feddbc2 --- /dev/null +++ b/sync.sh @@ -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}"