Add standalone Huoyi training project

This commit is contained in:
huangweifeng
2026-10-13 21:25:03 +08:00
commit 1aeeb34653
69 changed files with 7312 additions and 0 deletions
+18
View File
@@ -0,0 +1,18 @@
# copied and modified from https://github.com/NVIDIA/warp/blob/main/.gitattributes
* text=auto
*.sh text eol=LF
# # copied from https://github.com/isaac-sim/IsaacLab/blob/main/.gitattributes
# *.usd 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
# *.png filter=lfs diff=lfs merge=lfs -text
# *.jpg filter=lfs diff=lfs merge=lfs -text
# *.psd filter=lfs diff=lfs merge=lfs -text
# *.mp4 filter=lfs diff=lfs merge=lfs -text
# *.usda filter=lfs diff=lfs merge=lfs -text
# *.hdr filter=lfs diff=lfs merge=lfs -text
# *.pt filter=lfs diff=lfs merge=lfs -text
# *.jit filter=lfs diff=lfs merge=lfs -text
+39
View File
@@ -0,0 +1,39 @@
# Omniverse
**/*.dmp
**/.thumbs
# Python
.DS_Store
**/*.egg-info/
**/__pycache__/
**/.pytest_cache/
**/*.pyc
**/*.pb
# IDE
**/.idea/
**/.vscode/
# Don't ignore the top-level .vscode directory as it is
# used to configure VS Code settings
!.vscode
# Outputs
**/runs/*
**/logs/*
**/recordings/*
**/output/*
**/outputs/*
**/videos/*
**/wandb/*
**/.neptune/*
docker/artifacts/
*.tmp
# Isaac-Sim packman
_isaac_sim*
_repo
_build
.lastformat
**/usd/*
+10
View File
@@ -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
+220
View File
@@ -0,0 +1,220 @@
# Copyright (c) 2024-2025, The Isaac Lab Project Developers.
# All rights reserved.
#
# SPDX-License-Identifier: Apache-2.0
"""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 argparse
import os
import pathlib
import platform
import re
import sys
PROJECT_DIR = pathlib.Path(__file__).parents[2]
"""Path to the the project directory."""
try:
import isaacsim # noqa: F401
isaacsim_dir = os.environ.get("ISAAC_PATH", "")
except ModuleNotFoundError or ImportError:
# Create a parser to get the isaac-sim path
parser = argparse.ArgumentParser(description="Setup the VSCode settings for the project.")
parser.add_argument("--isaac_path", type=str, help="The absolute path to the Isaac Sim installation.")
args = parser.parse_args()
# parse the isaac-sim directory
isaacsim_dir = args.isaac_path
# check if the isaac-sim directory is provided
if not os.path.exists(isaacsim_dir):
raise FileNotFoundError(
f"Could not find the isaac-sim directory: {isaacsim_dir}. Please provide the correct path to the Isaac Sim"
" installation."
)
except EOFError:
print("Unable to trigger EULA acceptance. This is likely due to the script being run in a non-interactive shell.")
print("Please run the script in an interactive shell to accept the EULA.")
print("Skipping the setup of the VSCode settings...")
sys.exit(0)
# check if the isaac-sim directory exists
if not os.path.exists(isaacsim_dir):
raise FileNotFoundError(
f"Could not find the isaac-sim directory: {isaacsim_dir}. There are two possible reasons for this:"
"\n\t1. The Isaac Sim directory does not exist as provided CLI path."
"\n\t2. The script could import the 'isaacsim' package. This could be due to the 'isaacsim' package not being "
"installed in the Python environment.\n"
"\nPlease make sure that the Isaac Sim directory exists or that the 'isaacsim' package is installed."
)
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 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, PROJECT_DIR)
path_names = ['"${workspaceFolder}/' + rel_path + "/" + path_name + '"' for path_name in path_names]
else:
path_names = []
print(
f"[WARN] Could not find Isaac Sim VSCode settings: {isaacsim_vscode_filename}."
"\n\tThis will result in missing 'python.analysis.extraPaths' in the VSCode"
"\n\tsettings, which limits the functionality of the Python language server."
"\n\tHowever, it does not affect the functionality of the Isaac Lab project."
"\n\tWe are working on a fix for this issue with the Isaac Sim team."
)
# add the path names that are in the Isaac Lab extensions directory
isaaclab_extensions = os.listdir(os.path.join(PROJECT_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 = os.path.normpath(sys.executable)
# replace with Isaac Sim's python.sh or python.bat scripts to make sure python with correct
# source paths is set as default
if f"kit{os.sep}python{os.sep}bin{os.sep}python" in python_exe:
# Check if the OS is Windows or Linux to use appropriate shell file
if platform.system() == "Windows":
python_exe = python_exe.replace(f"kit{os.sep}python{os.sep}bin{os.sep}python3", "python.bat")
else:
python_exe = python_exe.replace(f"kit{os.sep}python{os.sep}bin{os.sep}python3", "python.sh")
# 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(PROJECT_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(PROJECT_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(PROJECT_DIR, ".vscode", "launch.json")
isaaclab_vscode_template_launch_filename = os.path.join(PROJECT_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()
+23
View File
@@ -0,0 +1,23 @@
# RL Training Developers and Contributors
This is the official list of RL Training Project developers and contributors.
To see the full list of contributors, please check the revision history in the source control.
Guidelines for modifications:
* Please keep the lists sorted alphabetically.
* Names should be added to this file as: *individual names* or *organizations*.
## Developers
* [Bo Peng](https://www.linkedin.com/in/bo-peng-647068272/)
## Contributors
* [Ziqi Fan](https://github.com/fan-ziqi)
* Zhongliang Pan
## Acknowledgements
+28
View File
@@ -0,0 +1,28 @@
BSD 3-Clause License
Copyright (c) 2025, DEEPRoboticsLab
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2024-2025 Ziqi Fan
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+180
View File
@@ -0,0 +1,180 @@
rl_training — 火蚁独立训练版本
> [!IMPORTANT]
> 本仓库是火蚁(Solenopsis)轮足平台的独立发布副本。火蚁专用训练、Stage 2、URDF与资产说明请先阅读 [README_HUOYI.md](README_HUOYI.md)。模型资产已直接包含在仓库中,不需要初始化 Git 子模块。
[![IsaacSim](https://img.shields.io/badge/IsaacSim-5.1.0-silver.svg)](https://docs.omniverse.nvidia.com/isaacsim/latest/overview.html)
[![Isaac Lab](https://img.shields.io/badge/IsaacLab-2.3.0-silver)](https://isaac-sim.github.io/IsaacLab)
[![Discord](https://img.shields.io/badge/-Discord-5865F2?style=flat&logo=Discord&logoColor=white)](https://discord.gg/gdM9mQutC8)
[![Python](https://img.shields.io/badge/python-3.11-blue.svg)](https://docs.python.org/3/whatsnew/3.11.html)
[![Linux platform](https://img.shields.io/badge/platform-linux--64-orange.svg)](https://releases.ubuntu.com/22.04/)
[![License](https://img.shields.io/badge/license-BSD%203--Clause-blue.svg)](https://opensource.org/license/bsd-3-clause)
## Tutorial Videos
We've released the following tutorials for training and deploying a reinforcement learning policy. Please check it out on [Bilibili](https://b23.tv/UoIqsFn) or [YouTube](https://youtube.com/playlist?list=PLy9YHJvMnjO0X4tx_NTWugTUMJXUrOgFH&si=pjUGF5PbFf3tGLFz)!
## Overview
**rl_training** is a RL training library for deeprobotics robots, based on IsaacLab. The table below lists all available environments:
| Robot Model | Environment Name (ID) | Screenshot |
|---------------------|------------------------------------------------------------|------------|
| [Deeprobotics Lite3](https://www.deeprobotics.cn/robot/index/product1.html) | Rough-Deeprobotics-Lite3-v0 | <img src="./docs/imgs/deeprobotics_lite3.png" alt="Lite3" width="300">
| [Deeprobotics M20](https://www.deeprobotics.cn/robot/index/lynx.html) | Rough-Deeprobotics-M20-v0 | <img src="./docs/imgs/deeprobotics_m20.png" alt="deeprobotics_m20" width="300">
> [!NOTE]
> If you want to deploy policies in mujoco or real robots, please use the corresponding deploy repo in [Deep Robotics Github Center](https://github.com/DeepRoboticsLab).
## Contribution
Everyone is welcome to contribute to this repo. If you discover a bug or optimize our training config, just submit a pull request and we will look into it.
## 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 installation as it simplifies calling Python scripts from the terminal.
- Clone this repository separately from the Isaac Lab installation (i.e. outside the `IsaacLab` directory):
```bash
git clone https://github.com/weifenghuang812/huoyi_training.git
cd huoyi_training
```
- Using a python interpreter that has Isaac Lab installed, install the library
```bash
python -m pip install -e source/rl_training
```
- Verify that the extension is correctly installed by running the following command to print all the available environments in the extension:
```bash
python scripts/tools/list_envs.py
```
<details>
<summary>Setup as Omniverse Extension (Optional, click to expand)</summary>
We provide an example UI extension that will load upon enabling your extension defined in `source/rl_training/rl_training/ui_extension_example.py`.
To enable your extension, follow these steps:
1. **Add the search path of your 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 `rl_trainingb/source`
- 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.
</details>
## Try examples
Deeprobotics Lite3:
```bash
# Train
python scripts/reinforcement_learning/rsl_rl/train.py --task=Rough-Deeprobotics-Lite3-v0 --headless
# Play
python scripts/reinforcement_learning/rsl_rl/play.py --task=Rough-Deeprobotics-Lite3-v0 --num_envs=10
```
Deeprobotics M20:
```bash
# Train
python scripts/reinforcement_learning/rsl_rl/train.py --task=Rough-Deeprobotics-M20-v0 --headless
# Play
python scripts/reinforcement_learning/rsl_rl/play.py --task=Rough-Deeprobotics-M20-v0 --num_envs=10
```
> [!NOTE]
> If you want to control a **SINGLE ROBOT** with the keyboard during playback, add `--keyboard` at the end of the play script.
>
> ```
> Key bindings:
> ====================== ========================= ========================
> Command Key (+ve axis) Key (-ve axis)
> ====================== ========================= ========================
> Move along x-axis Numpad 8 / Arrow Up Numpad 2 / Arrow Down
> Move along y-axis Numpad 4 / Arrow Right Numpad 6 / Arrow Left
> Rotate along z-axis Numpad 7 / Z Numpad 9 / X
> ====================== ========================= ========================
> ```
* Record video of a trained agent (requires installing `ffmpeg`), add `--video --video_length 200`
* Play/Train with 32 environments, add `--num_envs 32`
* Play on specific folder or checkpoint, add `--load_run run_folder_name --checkpoint model.pt`
* Resume training from folder or checkpoint, add `--resume --load_run run_folder_name --checkpoint model.pt`
## Trained Results
You can download our trained examples for reference of the training process. This should be expected if you run our training process properly. [M20](https://drive.google.com/file/d/1PAGIHOI6Lsge_JA9kU3w7RU-GyqXybob/view?usp=drive_link) and [Lite3](https://drive.google.com/file/d/1EgCVQA552x3YiaRhPRv675TSd01yqF0c/view?usp=drive_link)
## Multi-gpu acceleration
* To train with multiple GPUs, use the following command, where --nproc_per_node represents the number of available GPUs:
```bash
python -m torch.distributed.run --nnodes=1 --nproc_per_node=2 scripts/reinforcement_learning/rsl_rl/train.py --task=<ENV_NAME> --headless
python -m torch.distributed.run --nnodes=1 --nproc_per_node=2 scripts/reinforcement_learning/rsl_rl/train.py --task=Rough-Deeprobotics-Lite3-v0 --headless --distributed --num_envs=2048
```
* Note: each gpu will have the same number of envs specified in the config, to use the previous total number of envs, devide it by the number of gpus.
* To scale up training beyond multiple GPUs on a single machine, it is also possible to train across multiple nodes. To train across multiple nodes/machines, it is required to launch an individual process on each node.
For the master node, use the following command, where --nproc_per_node represents the number of available GPUs, and --nnodes represents the number of nodes:
```bash
python -m torch.distributed.run --nproc_per_node=2 --nnodes=2 --node_rank=0 --rdzv_id=123 --rdzv_backend=c10d --rdzv_endpoint=localhost:5555 scripts/reinforcement_learning/rsl_rl/train.py --task=<ENV_NAME> --headless --distributed
```
Note that the port (`5555`) can be replaced with any other available port.
For non-master nodes, use the following command, replacing `--node_rank` with the index of each machine:
```bash
python -m torch.distributed.run --nproc_per_node=2 --nnodes=2 --node_rank=1 --rdzv_id=123 --rdzv_backend=c10d --rdzv_endpoint=ip_of_master_machine:5555 scripts/reinforcement_learning/rsl_rl/train.py --task=<ENV_NAME> --headless --distributed
```
## Tensorboard
To view tensorboard, run:
```bash
tensorboard --logdir=logs
```
## 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"`.
**Note: Replace `<path-to-isaac-lab>` with your own IsaacLab path.**
```json
{
"python.languageServer": "Pylance",
"python.analysis.extraPaths": [
"${workspaceFolder}/source/rl_training",
"/<path-to-isaac-lab>/source/isaaclab",
"/<path-to-isaac-lab>/source/isaaclab_assets",
"/<path-to-isaac-lab>/source/isaaclab_mimic",
"/<path-to-isaac-lab>/source/isaaclab_rl",
"/<path-to-isaac-lab>/source/isaaclab_tasks",
]
}
```
### Clean USD Caches
Temporary USD files are generated in `/tmp/IsaacLab/usd_{date}_{time}_{random}` during simulation runs. These files can consume significant disk space and can be cleaned by:
```bash
rm -rf /tmp/IsaacLab/usd_*
```
## Acknowledgements
The project uses some code from the following open-source code repositories:
- [fan-ziqi/robot_lab](https://github.com/fan-ziqi/robot_lab)
+76
View File
@@ -0,0 +1,76 @@
# 火蚁轮足机器人强化学习项目
这是火蚁(Solenopsis)轮足平台当前可运行训练版本的独立发布副本。
## 包含内容
- Isaac Lab / RSL-RL 训练与播放脚本
- 火蚁 Stage 1 与 Stage 2 多地形任务配置
- 火蚁质量、惯量、电机和接触参数
- `Solenopsis.urdf`
- 当前 URDF 使用的轻量可视化 STL(碰撞体仍为简化几何体)
- 评估脚本和模型对比文档
为了保持仓库轻量,本仓库不包含训练日志、TensorBoard 数据、历史 checkpoint、原始高精度 CAD STL、旧 USD 备份或本地 Codex 快照。
## 克隆
```bash
git clone https://github.com/weifenghuang812/huoyi_training.git
cd huoyi_training
```
本仓库已经将火蚁模型直接包含在 `deep_robotics_model/` 中,不需要执行 `git submodule update`。
## 安装
先按项目原始 `README.md` 安装匹配版本的 Isaac Lab,然后在仓库根目录执行:
```bash
python -m pip install -e source/rl_training
```
## 从零训练 Stage 1
```bash
python scripts/reinforcement_learning/rsl_rl/train.py \
--task Rough-Deeprobotics-M20-v0 \
--headless
```
## 训练 Stage 2
Stage 2 应从 Stage 1 checkpoint 恢复:
```bash
python scripts/reinforcement_learning/rsl_rl/train.py \
--task Rough-Deeprobotics-M20-Support-v0 \
--headless \
--resume \
--load_run /绝对路径/到/stage1运行目录 \
--load_checkpoint model_xxx.pt
```
## 播放
```bash
python scripts/reinforcement_learning/rsl_rl/play.py \
--task Rough-Deeprobotics-M20-v0 \
--checkpoint /绝对路径/到/model_xxx.pt
```
键盘控制时添加:
```bash
--keyboard
```
## 资产说明
活动模型路径为:
```text
deep_robotics_model/M20/M20_urdf/urdf/Solenopsis.urdf
```
训练加载 URDF;碰撞体使用方块和圆柱简化,可视化使用轻量 STL。
+1
View File
@@ -0,0 +1 @@
1.0.0
+28
View File
@@ -0,0 +1,28 @@
BSD 3-Clause License
Copyright (c) 2024, DeepRoboticsLab
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,92 @@
{
"source_dir": "/home/ycpy/huangweifeng/hwf_workspace/rl_training_huoyi/rl_training/deep_robotics_model/M20/M20_urdf/meshes_solenopsis",
"grid_mm": 2.0,
"maximum_vertex_displacement_mm": 1.7320508075688772,
"source_faces": 2146372,
"optimized_faces": 234820,
"source_bytes": 107319356,
"optimized_bytes": 11741756,
"meshes": [
{
"file": "base_link.STL",
"source_vertices": 568586,
"source_faces": 1140292,
"optimized_vertices": 35724,
"optimized_faces": 86170,
"source_bytes": 57014684,
"optimized_bytes": 4308584
},
{
"file": "fl_knee.STL",
"source_vertices": 54172,
"source_faces": 108311,
"optimized_vertices": 6368,
"optimized_faces": 14405,
"source_bytes": 5415634,
"optimized_bytes": 720334
},
{
"file": "fl_wheel.STL",
"source_vertices": 71661,
"source_faces": 143210,
"optimized_vertices": 11121,
"optimized_faces": 22793,
"source_bytes": 7160584,
"optimized_bytes": 1139734
},
{
"file": "fr_knee.STL",
"source_vertices": 54172,
"source_faces": 108311,
"optimized_vertices": 6363,
"optimized_faces": 14398,
"source_bytes": 5415634,
"optimized_bytes": 719984
},
{
"file": "fr_wheel.STL",
"source_vertices": 71668,
"source_faces": 143224,
"optimized_vertices": 11122,
"optimized_faces": 22796,
"source_bytes": 7161284,
"optimized_bytes": 1139884
},
{
"file": "hl_knee.STL",
"source_vertices": 54172,
"source_faces": 108311,
"optimized_vertices": 6340,
"optimized_faces": 14346,
"source_bytes": 5415634,
"optimized_bytes": 717384
},
{
"file": "hl_wheel.STL",
"source_vertices": 71660,
"source_faces": 143208,
"optimized_vertices": 11120,
"optimized_faces": 22790,
"source_bytes": 7160484,
"optimized_bytes": 1139584
},
{
"file": "hr_knee.STL",
"source_vertices": 54172,
"source_faces": 108311,
"optimized_vertices": 6331,
"optimized_faces": 14331,
"source_bytes": 5415634,
"optimized_bytes": 716634
},
{
"file": "hr_wheel.STL",
"source_vertices": 71653,
"source_faces": 143194,
"optimized_vertices": 11121,
"optimized_faces": 22791,
"source_bytes": 7159784,
"optimized_bytes": 1139634
}
]
}
@@ -0,0 +1,292 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Solenopsis fire-ant, generated from the 2026 SolidWorks STEP assembly.
Units: metre, kilogram, radian, kg*m^2.
Coordinate conversion: (x,y,z)_URDF = (z,x,y)_SolidWorks.
All inertia tensors are about each rigid body's own centre of mass.
Visual geometry uses the STEP-derived STL files in
../meshes_solenopsis_visual at a millimetre-to-metre scale of 0.001.
Collision geometry remains primitive boxes and cylinders.
The 0.127 kg gap between the supplied rigid-group masses and the supplied
whole-robot mass is lumped into base_link at the reported base COM. Thus
base mass is 17.274 + 0.127 = 17.401 kg, while its reported COM and inertia
tensor are unchanged. No spatial distribution is invented for that mass.
-->
<robot name="Solenopsis_fire_ant">
<link name="base_link">
<inertial>
<origin xyz="-0.000072 0.000171 0.076892" rpy="0 0 0"/>
<mass value="17.401"/>
<inertia ixx="0.133964838" ixy="-0.000732466" ixz="0.000157104"
iyy="0.363696493" iyz="0.000165399" izz="0.450717599"/>
</inertial>
<visual>
<geometry>
<mesh filename="../meshes_solenopsis_visual/base_link.STL" scale="0.001 0.001 0.001"/>
</geometry>
</visual>
<collision>
<origin xyz="0 0 0.0788" rpy="0 0 0"/>
<geometry>
<box size="0.706 0.310 0.1714"/>
</geometry>
</collision>
</link>
<!-- Front-left. Left-link tensors use the exact proper rotation
(x,y,z)_link = (-z,-x,y)_SolidWorks-module. -->
<link name="fl_knee">
<inertial>
<origin xyz="-0.000040 -0.001088 -0.158988" rpy="0 0 0"/>
<mass value="0.955"/>
<inertia ixx="0.014124479" ixy="0.000002919" ixz="0.000004851"
iyy="0.014470094" iyz="-0.000261898" izz="0.000478899"/>
</inertial>
<visual>
<geometry>
<mesh filename="../meshes_solenopsis_visual/fl_knee.STL" scale="0.001 0.001 0.001"/>
</geometry>
</visual>
<collision>
<origin xyz="0 -0.0005 -0.15575" rpy="0 0 0"/>
<geometry>
<box size="0.088 0.031 0.3765"/>
</geometry>
</collision>
<collision>
<origin xyz="0 -0.02625 0" rpy="1.570796326795 0 0"/>
<geometry>
<cylinder length="0.0205" radius="0.01995"/>
</geometry>
</collision>
</link>
<joint name="fl_knee_joint" type="revolute">
<origin xyz="0.117577164466 0.167 0.070" rpy="0 0 0"/>
<parent link="base_link"/>
<child link="fl_knee"/>
<axis xyz="0 -1 0"/>
<limit lower="-2.792" upper="2.809" effort="75.0" velocity="17.8024"/>
</joint>
<link name="fl_wheel">
<inertial>
<origin xyz="-0.000002 0.059326 -0.000009" rpy="0 0 0"/>
<mass value="1.340"/>
<inertia ixx="0.003296552" ixy="0.000000144" ixz="-0.000000077"
iyy="0.005826562" iyz="0.000000424" izz="0.003296995"/>
</inertial>
<visual>
<geometry>
<mesh filename="../meshes_solenopsis_visual/fl_wheel.STL" scale="0.001 0.001 0.001"/>
</geometry>
</visual>
<collision>
<origin xyz="0 0.0457795 0" rpy="1.570796326795 0 0"/>
<geometry>
<cylinder length="0.082959" radius="0.098804"/>
</geometry>
</collision>
</link>
<joint name="fl_wheel_joint" type="continuous">
<origin xyz="0 0 -0.300" rpy="0 0 0"/>
<parent link="fl_knee"/>
<child link="fl_wheel"/>
<axis xyz="0 -1 0"/>
<limit effort="28.68" velocity="109.956"/>
</joint>
<!-- Front-right. Right-link tensors use the exact proper rotation
(x,y,z)_link = (z,x,y)_SolidWorks-module. -->
<link name="fr_knee">
<inertial>
<origin xyz="0.000040 0.001088 -0.158988" rpy="0 0 0"/>
<mass value="0.955"/>
<inertia ixx="0.014124479" ixy="0.000002919" ixz="-0.000004851"
iyy="0.014470094" iyz="0.000261898" izz="0.000478899"/>
</inertial>
<visual>
<geometry>
<mesh filename="../meshes_solenopsis_visual/fr_knee.STL" scale="0.001 0.001 0.001"/>
</geometry>
</visual>
<collision>
<origin xyz="0 0.0005 -0.15575" rpy="0 0 0"/>
<geometry>
<box size="0.088 0.031 0.3765"/>
</geometry>
</collision>
<collision>
<origin xyz="0 0.02625 0" rpy="1.570796326795 0 0"/>
<geometry>
<cylinder length="0.0205" radius="0.01995"/>
</geometry>
</collision>
</link>
<joint name="fr_knee_joint" type="revolute">
<origin xyz="0.117577164466 -0.167 0.070" rpy="0 0 0"/>
<parent link="base_link"/>
<child link="fr_knee"/>
<axis xyz="0 -1 0"/>
<limit lower="-2.792" upper="2.809" effort="75.0" velocity="17.8024"/>
</joint>
<link name="fr_wheel">
<inertial>
<origin xyz="0.000002 -0.059326 -0.000009" rpy="0 0 0"/>
<mass value="1.340"/>
<inertia ixx="0.003296552" ixy="0.000000144" ixz="0.000000077"
iyy="0.005826562" iyz="-0.000000424" izz="0.003296995"/>
</inertial>
<visual>
<geometry>
<mesh filename="../meshes_solenopsis_visual/fr_wheel.STL" scale="0.001 0.001 0.001"/>
</geometry>
</visual>
<collision>
<origin xyz="0 -0.0457795 0" rpy="1.570796326795 0 0"/>
<geometry>
<cylinder length="0.082959" radius="0.098804"/>
</geometry>
</collision>
</link>
<joint name="fr_wheel_joint" type="continuous">
<origin xyz="0 0 -0.300" rpy="0 0 0"/>
<parent link="fr_knee"/>
<child link="fr_wheel"/>
<axis xyz="0 -1 0"/>
<limit effort="28.68" velocity="109.956"/>
</joint>
<link name="hl_knee">
<inertial>
<origin xyz="-0.000040 -0.001088 -0.158988" rpy="0 0 0"/>
<mass value="0.955"/>
<inertia ixx="0.014124479" ixy="0.000002919" ixz="0.000004851"
iyy="0.014470094" iyz="-0.000261898" izz="0.000478899"/>
</inertial>
<visual>
<geometry>
<mesh filename="../meshes_solenopsis_visual/hl_knee.STL" scale="0.001 0.001 0.001"/>
</geometry>
</visual>
<collision>
<origin xyz="0 -0.0005 -0.15575" rpy="0 0 0"/>
<geometry>
<box size="0.088 0.031 0.3765"/>
</geometry>
</collision>
<collision>
<origin xyz="0 -0.02625 0" rpy="1.570796326795 0 0"/>
<geometry>
<cylinder length="0.0205" radius="0.01995"/>
</geometry>
</collision>
</link>
<joint name="hl_knee_joint" type="revolute">
<origin xyz="-0.117577164466 0.167 0.070" rpy="0 0 0"/>
<parent link="base_link"/>
<child link="hl_knee"/>
<axis xyz="0 -1 0"/>
<limit lower="-2.809" upper="2.792" effort="75.0" velocity="17.8024"/>
</joint>
<link name="hl_wheel">
<inertial>
<origin xyz="-0.000002 0.059326 -0.000009" rpy="0 0 0"/>
<mass value="1.340"/>
<inertia ixx="0.003296552" ixy="0.000000144" ixz="-0.000000077"
iyy="0.005826562" iyz="0.000000424" izz="0.003296995"/>
</inertial>
<visual>
<geometry>
<mesh filename="../meshes_solenopsis_visual/hl_wheel.STL" scale="0.001 0.001 0.001"/>
</geometry>
</visual>
<collision>
<origin xyz="0 0.0457795 0" rpy="1.570796326795 0 0"/>
<geometry>
<cylinder length="0.082959" radius="0.098804"/>
</geometry>
</collision>
</link>
<joint name="hl_wheel_joint" type="continuous">
<origin xyz="0 0 -0.300" rpy="0 0 0"/>
<parent link="hl_knee"/>
<child link="hl_wheel"/>
<axis xyz="0 -1 0"/>
<limit effort="28.68" velocity="109.956"/>
</joint>
<link name="hr_knee">
<inertial>
<origin xyz="0.000040 0.001088 -0.158988" rpy="0 0 0"/>
<mass value="0.955"/>
<inertia ixx="0.014124479" ixy="0.000002919" ixz="-0.000004851"
iyy="0.014470094" iyz="0.000261898" izz="0.000478899"/>
</inertial>
<visual>
<geometry>
<mesh filename="../meshes_solenopsis_visual/hr_knee.STL" scale="0.001 0.001 0.001"/>
</geometry>
</visual>
<collision>
<origin xyz="0 0.0005 -0.15575" rpy="0 0 0"/>
<geometry>
<box size="0.088 0.031 0.3765"/>
</geometry>
</collision>
<collision>
<origin xyz="0 0.02625 0" rpy="1.570796326795 0 0"/>
<geometry>
<cylinder length="0.0205" radius="0.01995"/>
</geometry>
</collision>
</link>
<joint name="hr_knee_joint" type="revolute">
<origin xyz="-0.117577164466 -0.167 0.070" rpy="0 0 0"/>
<parent link="base_link"/>
<child link="hr_knee"/>
<axis xyz="0 -1 0"/>
<limit lower="-2.809" upper="2.792" effort="75.0" velocity="17.8024"/>
</joint>
<link name="hr_wheel">
<inertial>
<origin xyz="0.000002 -0.059326 -0.000009" rpy="0 0 0"/>
<mass value="1.340"/>
<inertia ixx="0.003296552" ixy="0.000000144" ixz="0.000000077"
iyy="0.005826562" iyz="-0.000000424" izz="0.003296995"/>
</inertial>
<visual>
<geometry>
<mesh filename="../meshes_solenopsis_visual/hr_wheel.STL" scale="0.001 0.001 0.001"/>
</geometry>
</visual>
<collision>
<origin xyz="0 -0.0457795 0" rpy="1.570796326795 0 0"/>
<geometry>
<cylinder length="0.082959" radius="0.098804"/>
</geometry>
</collision>
</link>
<joint name="hr_wheel_joint" type="continuous">
<origin xyz="0 0 -0.300" rpy="0 0 0"/>
<parent link="hr_knee"/>
<child link="hr_wheel"/>
<axis xyz="0 -1 0"/>
<limit effort="28.68" velocity="109.956"/>
</joint>
</robot>
+18
View File
@@ -0,0 +1,18 @@
# Robot Models Repository
This repository contains 3D models for three Deep Robotics robots: **Lite3**, **M20**, and **X30**. Each model is provided in MJCF, URDF, and USD formats. To visualize the model and do some measuring, I recommend using this [tool](https://viewer.robotsfan.com/). Just drag your folder to the web page and you can see everything.
<span style="color: red;">**Note**</span>: This repository only contains low resolution files. To download the high resolution files, please use [this link](https://drive.google.com/drive/folders/1EOELXUYSBPEJeD0rUIkJxnlLMDr6IHBV?usp=sharing)
## Model Overview
**Note**: High resolution models are good for new simulators like isaaclab/isaacsim but old simulators like pybullet cannot open files this big.
| Robot Model | High Resolution Image | Low Resolution Image |
|:-----------:|:---------------------:|:--------------------:|
| **Lite3** | <img src="./images/lite3_high_res.png" width="200" /><br>Lite3/Lite3_urdf/urdf/Lite3_high_res.urdf | <img src="./images/lite3.png" width="200" /><br>Lite3/Lite3_urdf/urdf/Lite3.urdf |
| **X30** | <img src="./images/x30_high_res.png" width="200" /><br>X30/X30_urdf/urdf/X30_high_res.urdf | <img src="./images/x30.png" width="200" /><br>X30/X30_urdf/urdf/X30.urdf |
| **M20** | <img src="./images/m20_high_res.png" width="200" /><br>M20/M20_urdf/urdf/M20_high_res.urdf | <img src="./images/m20.png" width="200" /><br>M20/M20_urdf/urdf/M20.urdf |
## Contributors
See the [Contributors](Contributors.md) page for a list of contributors.
+178
View File
@@ -0,0 +1,178 @@
# 火蚁样机参数简表
本文档依据当前火蚁仿真模型与训练配置整理:
- URDF: `deep_robotics_model/M20/M20_urdf/urdf/M20.urdf`
- 执行器配置: `source/rl_training/rl_training/assets/deeprobotics.py`
- 训练配置: `source/rl_training/rl_training/tasks/manager_based/locomotion/velocity/config/wheeled/deeprobotics_m20/rough_env_cfg.py`
## 1. 结构形式
火蚁当前为 8 自由度轮腿机器人
## 2. 质量参数
整机仿真总质量约:23.690 kg
| 部件 | 单个质量 | 数量 | 小计 |
| --- | ---: | ---: | ---: |
| 机身 base_link | 15.882 kg | 1 | 15.882 kg |
| 膝部连杆(按 P65MAX-S 质量建模) | 0.612 kg | 4 | 2.448 kg |
| 轮毂电机与轮胎(W190-S) | 1.340 kg | 4 | 5.360 kg |
## 3. 尺寸参数
机身主体:
长 x 宽 x 高 = 0.60 m x 0.32 m x 0.12 m
轮子:
半径 = 0.096 m
直径 = 0.192 m
轴向总尺寸 = 0.083 m(手册工程图标注 82.95 mm)
```
膝部连杆简化尺寸:
主连杆 box = 0.05 m x 0.024 m x 0.20 m
末端圆柱半径 = 0.04 m
末端圆柱长度 = 0.09 m
零关节角几何尺寸:
```text
轴距 = 0.60 m
轮距 = 0.50 m
轮心相对机身中心高度 = -0.21 m
```
训练初始姿态下,前膝为 `+0.5 rad`,后膝为 `-0.5 rad`:
```text
有效轴距约 = 0.801 m
轮距 = 0.50 m
机身中心接地高度约 = 0.280 m
机身底部离地约 = 0.220 m
```
## 4. 电机与执行器参数
### 膝关节电机
仿真限制:
```text
型号 = P65MAX-S
额定扭矩 = 21.3 Nm
峰值扭矩 = 75.0 Nm
速度上限 = 17.8024 rad/s ≈ 170 rpm
```
IsaacLab 控制参数:
```text
位置刚度 stiffness = 80.0
阻尼 damping = 2.0
控制延迟 = 0 ~ 5 step
```
当前 URDF 与 Isaac Lab actuator 均使用相同的峰值扭矩和速度上限。
### 轮端电机
仿真实际执行器限制:
```text
型号 = W190-S
额定扭矩 = 8.7 Nm
峰值扭矩 = 28.68 Nm
额定点转速 = 94.457 rad/s ≈ 902 rpm
速度上限 = 109.956 rad/s ≈ 1050 rpm
理论空载轮缘线速度 = 10.56 m/s
```
URDF 中轮关节写有:
```text
effort = 28.68 Nm
velocity = 109.956 rad/s
```
URDF 与 Isaac Lab actuator 的限制现已保持一致。
## 5. 训练需求
Stage 1 基础运动:
```text
前后速度 lin_vel_x = -2.0 ~ 2.0 m/s
侧向速度 lin_vel_y = 0
转向 yaw_rate = -1.5 ~ 1.5 rad/s
```
Stage 2 超支撑越障:
```text
前进速度 lin_vel_x = 0.25 ~ 1.2 m/s
侧向速度 lin_vel_y = 0
转向 yaw_rate = -0.6 ~ 0.6 rad/s
```
Stage 2 主要地形难度:
```text
boxes 高度 = 0.03 ~ 0.22 m
上台阶高度 = 0.04 ~ 0.24 m
下台阶高度 = 0.04 ~ 0.20 m
随机粗糙高度 = 0.01 ~ 0.14 m
```
## 6. 样机设计重点
优先级建议:
1. 膝关节峰值扭矩优先于轮端极速。
2. 轮端要保证低速大扭矩,避免顶障碍时堵转。
3. 机身底部、膝部、轮架必须能承受超支撑接触冲击。
4. 轮胎材料要保证摩擦和一定缓冲,不能只看刚性轮模型。
5. 电机驱动器需要考虑峰值电流、连续散热和限流策略。
## 7. 当前仿真限制
接触材质配置:
| 接触对象 | 静摩擦随机范围 | 动摩擦随机范围 | 恢复系数随机范围 |
| --- | ---: | ---: | ---: |
| W190-S 轮子 | 0.9 ~ 1.4 | 0.6 ~ 0.9 | 0.0 ~ 0.05 |
| base_link 与 knee links | 0.4 ~ 0.8 | 0.25 ~ 0.4 | 0.0 ~ 0.05 |
地形静、动摩擦均为 1.0,摩擦使用乘法组合,因此轮地有效摩擦范围与轮子材质随机范围一致。火蚁任务将地形恢复系数覆盖为 0.0,因此轮地碰撞的有效恢复系数为 0。质量与惯量随机化保持启用。
当前模型不能直接等价为最终硬件设计:
- 几何为简化 box/cylinder,不是完整 CAD。
- 没有真实电机扭矩-转速曲线。
- 没有电池、电流限制、热衰减。
- 没有减速器效率、背隙、柔性结构。
- 没有轮胎变形模型。
- 接触与摩擦比真实世界更理想。
- 轮关节 URDF effort 与 IsaacLab actuator effort 不一致,训练实际以 actuator 为准。
## 8. 初版样机目标
建议以如下指标作为第一版样机基准:
```text
整机仿真质量:约 23.69 kg
轮径:0.192 m
轮距:0.50 m
有效轴距:约 0.80 m
膝关节峰值扭矩:75 Nm
轮端峰值扭矩:28.68 Nm
机身底部离地:约 0.22 m
目标越障高度:0.20 ~ 0.24 m
```
Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

+51
View File
@@ -0,0 +1,51 @@
# P65MAX-S / W190-S 模型更新对比
数据来源:智身科技《关节选型手册 2026》。本次将简化 M20 URDF 的关节电机替换为 P65MAX-S,将轮毂电机替换为 W190-S。
## 关节与执行器
| 参数 | 修改前 | 修改后 |
| --- | ---: | ---: |
| 膝关节峰值扭矩 | 76.4 Nm | 75.0 Nm |
| 膝关节速度上限 | 22.4 rad/s(约 214 rpm) | 17.8024 rad/s(170 rpm) |
| 单个 knee link 质量 | 1.000 kg | 0.612 kg |
| 轮关节 URDF 扭矩上限 | 40.0 Nm | 28.68 Nm |
| 轮关节训练执行器扭矩上限 | 21.6 Nm | 28.68 Nm |
| 轮关节速度上限 | 79.3 rad/s(约 757 rpm) | 109.956 rad/s(1050 rpm) |
## 轮毂几何与质量
| 参数 | 修改前 | 修改后 |
| --- | ---: | ---: |
| 轮半径 | 0.140 m | 0.096 m |
| 轮直径 | 0.280 m | 0.192 m |
| 轴向尺寸 | 0.090 m | 0.083 m |
| 单轮质量 | 1.400 kg | 1.340 kg |
| 绕轮轴转动惯量 | 0.013700 kg·m² | 0.006174720 kg·m² |
| 径向转动惯量 | 0.007500 kg·m² | 0.003856632 kg·m² |
| 空载理论轮缘速度 | 11.10 m/s | 10.56 m/s |
W190-S 的新转动惯量按质量 1.34 kg、半径 0.096 m、轴向尺寸 0.083 m 的均匀实心圆柱重新计算。
## 整机影响
| 参数 | 修改前 | 修改后 | 变化 |
| --- | ---: | ---: | ---: |
| URDF 总质量 | 25.482 kg | 23.690 kg | -1.792 kg |
| 初始生成高度 | 0.350 m | 0.306 m | -0.044 m |
| 初始姿态稳定后机身中心高度 | 约 0.324 m | 约 0.280 m | -0.044 m |
| 初始姿态机身底部离地 | 约 0.264 m | 约 0.220 m | -0.044 m |
| 轮距 | 0.500 m | 0.500 m | 不变 |
| 有效轴距 | 约 0.801 m | 约 0.801 m | 不变 |
## 同步修改位置
- `deep_robotics_model/M20/M20_urdf/urdf/M20.urdf`:质量、惯量、轮子几何、关节限制。
- `source/rl_training/rl_training/assets/deeprobotics.py`:训练执行器限制和初始生成高度。
- `source/rl_training/rl_training/tasks/manager_based/locomotion/velocity/config/wheeled/deeprobotics_m20/rough_env_cfg.py`:差速转向奖励和轮滑观测所用轮半径。
## 建模边界
- 简化 URDF 没有独立的电机刚体,因此将 knee link 质量直接设为 P65MAX-S 的 0.612 kg,并按质量比例缩放原 knee link 惯量。
- W190-S 手册给出的是约 83 mm 的轴向总尺寸,简化圆柱模型将其作为轮宽。
- 当前执行器仍使用固定扭矩与固定速度上限,没有拟合手册中的完整扭矩—转速曲线、热衰减或电流限制。
+114
View File
@@ -0,0 +1,114 @@
# Solenopsis CAD URDF 与旧 M20 简化 URDF 对比
对比对象:
- 旧模型:`deep_robotics_model/M20/M20_urdf/urdf/M20.urdf`
- 新模型:`deep_robotics_model/M20/M20_urdf/urdf/Solenopsis.urdf`
- CAD 来源:`Solenopsis-00 火蚁-四单臂轮足平台总成.STEP`
## 质量闭合
用户提供的刚体分组质量为:机身组 17.274 kg、单个摆臂组 0.955 kg、单个轮组 1.340 kg;整机质量为 26.581 kg。
```text
17.274 + 4 × 0.955 + 4 × 1.340 = 26.454 kg
26.581 - 26.454 = 0.127 kg
```
按确认结果,将差额 0.127 kg(即 127 g,不是 0.127 g)合并到 `base_link`,因此新 `base_link` 质量为 17.401 kg。该差额等效放在已报告的机身组质心处,所以不改变机身组质心坐标和惯量张量;没有假定未知零部件的空间分布。
| 刚体 | 旧模型单体质量 | 新模型单体质量 | 单体变化 |
| --- | ---: | ---: | ---: |
| `base_link` | 15.882 kg | 17.401 kg | +1.519 kg(+9.5643%) |
| 每个 `*_knee` | 0.612 kg | 0.955 kg | +0.343 kg(+56.0458%) |
| 每个 `*_wheel` | 1.340 kg | 1.340 kg | 0 |
| 整机 | 23.690 kg | 26.581 kg | +2.891 kg(+12.2035%) |
## 坐标换算
SolidWorks 总装坐标为 Y 轴向上;现有训练模型采用 ROS/URDF 的 Z 轴向上。新模型使用以下右手坐标变换:
```text
(x, y, z)_URDF = (z, x, y)_SolidWorks
```
摆臂和轮子的质量属性原始坐标系是运动模组坐标系,X 轴为电机旋转轴,Y 轴沿轮心指向关节中心。为了保持现有控制器的关节符号约定,新 URDF 统一定义:
- 四个关节轴均为 `(0, -1, 0)`;
- `q = 0` 时四个摆臂均竖直向下;
- 关节中心到轮轴中心的距离为 CAD 中的精确值 0.300 m;
- 左右侧采用不同的正规旋转矩阵变换质量属性,没有使用镜像矩阵变换惯量。
## 质心与惯量
所有新惯量均为“关于各自刚体质心、并与对应 URDF link 坐标轴对齐”的惯量,单位为 kg·m²。SolidWorks 的 kg·mm² 已严格乘以 `10^-6`,并按 `I_URDF = R I_SW R^T` 旋转。
### base_link
| 参数 | 旧模型 | 新模型 |
| --- | ---: | ---: |
| 质心 x | 0.003551059 m | -0.000072 m |
| 质心 y | 0.001650758 m | 0.000171 m |
| 质心 z | -0.003464621 m | 0.076892 m |
| Ixx | 0.088078700 | 0.133964838 |
| Iyy | 0.533816000 | 0.363696493 |
| Izz | 0.563915000 | 0.450717599 |
| Ixy | 0.001396000 | -0.000732466 |
| Ixz | 0.000111000 | 0.000157104 |
| Iyz | 0.000519000 | 0.000165399 |
### 单个 knee
新左侧 knee 质心为 `(-0.000040, -0.001088, -0.158988) m`,新右侧 knee 质心为 `(0.000040, 0.001088, -0.158988) m`。
| 参数 | 旧模型代表值 | 新模型左侧 | 新模型右侧 |
| --- | ---: | ---: | ---: |
| Ixx | 0.005202000 | 0.014124479 | 0.014124479 |
| Iyy | 0.005202000 | 0.014470094 | 0.014470094 |
| Izz | 0.000734400 | 0.000478899 | 0.000478899 |
| Ixy | ±0.000024480 | 0.000002919 | 0.000002919 |
| Ixz | ±0.000091800 | 0.000004851 | -0.000004851 |
| Iyz | ±0.000673200 | -0.000261898 | 0.000261898 |
### 单个 wheel
新左轮质心为 `(-0.000002, 0.059326, -0.000009) m`,新右轮质心为 `(0.000002, -0.059326, -0.000009) m`。这保留了 SolidWorks 报告的轮组轴向质心偏置,不再把轮组质心强制放在关节原点。
| 参数 | 旧均匀圆柱模型 | 新模型左侧 | 新模型右侧 |
| --- | ---: | ---: | ---: |
| Ixx | 0.003856632 | 0.003296552 | 0.003296552 |
| Iyy(轮轴) | 0.006174720 | 0.005826562 | 0.005826562 |
| Izz | 0.003856632 | 0.003296995 | 0.003296995 |
| Ixy | 0 | 0.000000144 | 0.000000144 |
| Ixz | 0 | -0.000000077 | 0.000000077 |
| Iyz | 0 | 0.000000424 | -0.000000424 |
## 关节几何
| 参数 | 旧简化模型 | 新 CAD 模型 |
| --- | ---: | ---: |
| 前/后关节中心 x | ±0.300 m | ±0.117577164466 m |
| 左/右关节中心 y | ±0.140 m | ±0.167 m |
| 关节中心 z | 0 m | 0.070 m |
| 膝关节至轮轴向量 | `(0, ±0.110, -0.210) m` | `(0, 0, -0.300) m` |
| 膝关节至轮轴距离 | 0.237065 m | 0.300000 m |
| 关节轴 | `(0, -1, 0)` | `(0, -1, 0)` |
| 自由度 | 8 | 8 |
关节名称、顺序、扭矩上限和速度上限均保持不变,因此软件接口仍是四个 knee 加四个 wheel。不过,质量、质心、惯量、关节位置和杆长都有实质变化,旧策略不能被视为动力学等价策略,应重新训练或至少重新做系统性验证。
## 显示与碰撞几何
新 URDF 使用从 STEP 总成拆分得到的 9 个 STL 作为 visual:一个机身、四个摆臂、四个轮组。STL 保留毫米坐标,URDF 使用 `0.001` 比例换算到米。当前训练引用 `meshes_solenopsis_visual` 中的可视化网格;高精度原始导出保留在 `meshes_solenopsis` 中。
collision 不使用 STL,只使用方块和圆柱,尺寸来自 CAD 外廓:
- `base_link`:一个 `0.706 × 0.310 × 0.1714 m` 方块;
- 每个 `*_knee`:一个 `0.088 × 0.031 × 0.3765 m` 摆臂方块,加一个 P65 转动端圆柱;
- 每个 `*_wheel`:半径 `0.098804 m`、轴向宽度 `0.082959 m` 的圆柱。
训练可视化网格约 23.5 万个三角形、11.7 MB,顶点相对高精度 STL 的最大量化位移不超过 1.733 mm。非闭合边、内部电气件和紧固件不会参与碰撞求解。这样既保留 CAD 外观,又避免 214 万面高精度 STL 拖慢 Isaac Sim 导入,同时让并行训练继续使用稳定的基础几何碰撞体。
## 当前切换状态
旧 `M20.urdf` 没有被覆盖,IsaacLab训练入口已经切换到 `Solenopsis.urdf`。导入时,visual加载STEP拆分得到的STL用于正常显示,collision只加载方块和圆柱参与物理计算。
+85
View File
@@ -0,0 +1,85 @@
[build-system]
requires = ["setuptools", "toml"]
build-backend = "setuptools.build_meta"
[tool.isort]
atomic = true
profile = "black"
line_length = 120
py_version = 310
skip_glob = ["docs/*", "logs/*"]
group_by_package = true
sections = [
"FUTURE",
"STDLIB",
"THIRDPARTY",
"ISAACLABPARTY",
"FIRSTPARTY",
"LOCALFOLDER",
]
extra_standard_library = [
"numpy",
"h5py",
"open3d",
"torch",
"tensordict",
"bpy",
"matplotlib",
"gymnasium",
"gym",
"scipy",
"hid",
"yaml",
"prettytable",
"toml",
"trimesh",
"tqdm",
"psutil",
]
known_thirdparty = [
"isaacsim.core.api",
"omni.replicator.core",
"pxr",
"omni.kit.*",
"warp",
"carb",
"Semantics",
]
known_isaaclabparty = [
"isaaclab",
"isaaclab_tasks",
"isaaclab_assets",
"isaaclab_mimic",
"isaaclab_rl"
]
# Modify the following to include the package names of your first-party code
known_firstparty = "rl_training"
known_local_folder = "config"
[tool.pyright]
exclude = [
"**/__pycache__",
"**/docs",
"**/logs",
".git",
".vscode",
]
typeCheckingMode = "basic"
pythonVersion = "3.11"
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"
@@ -0,0 +1,44 @@
# Copyright (c) 2025 Deep Robotics
# SPDX-License-Identifier: BSD 3-Clause
# Copyright (c) 2024-2025 Ziqi Fan
# SPDX-License-Identifier: Apache-2.0
import torch
import isaaclab.utils.math as math_utils
def camera_follow(env):
if not hasattr(camera_follow, "smooth_camera_positions"):
camera_follow.smooth_camera_positions = []
robot_pos = env.unwrapped.scene["robot"].data.root_pos_w[0]
robot_quat = env.unwrapped.scene["robot"].data.root_quat_w[0]
camera_offset = torch.tensor([-3.0, 0.0, 0.5], dtype=torch.float32, device=env.device)
camera_pos = math_utils.transform_points(
camera_offset.unsqueeze(0), pos=robot_pos.unsqueeze(0), quat=robot_quat.unsqueeze(0)
).squeeze(0)
# camera_pos[2] = torch.clamp(camera_pos[2], min=0.1)
window_size = 50
camera_follow.smooth_camera_positions.append(camera_pos)
if len(camera_follow.smooth_camera_positions) > window_size:
camera_follow.smooth_camera_positions.pop(0)
smooth_camera_pos = torch.mean(torch.stack(camera_follow.smooth_camera_positions), dim=0)
env.unwrapped.viewport_camera_controller.set_view_env_index(env_index=0)
env.unwrapped.viewport_camera_controller.update_view_location(
eye=smooth_camera_pos.cpu().numpy(), lookat=robot_pos.cpu().numpy()
)
import os
import pickle
from typing import Any
def reindex_feet(vec):
return vec[:, [1, 0, 3, 2]]
def reindex(vec):
return vec[:, [3, 4, 5, 0, 1, 2, 9, 10, 11, 6, 7, 8]]
@@ -0,0 +1,97 @@
# Copyright (c) 2025 Deep Robotics
# SPDX-License-Identifier: BSD 3-Clause
# Copyright (c) 2024-2025 Ziqi Fan
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) 2024-2025, The Isaac Lab Project Developers.
# All rights reserved.
#
# SPDX-License-Identifier: Apache-2.0
from __future__ import annotations
import argparse
import random
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from isaaclab_rl.rsl_rl import RslRlOnPolicyRunnerCfg
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) -> RslRlOnPolicyRunnerCfg:
"""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: RslRlOnPolicyRunnerCfg = 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: RslRlOnPolicyRunnerCfg, 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.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,716 @@
# Copyright (c) 2025 Deep Robotics
# SPDX-License-Identifier: BSD 3-Clause
"""Evaluate an RSL-RL checkpoint with criterion metrics migrated from legged_gym."""
from __future__ import annotations
import argparse
import copy
import json
import os
import sys
from datetime import datetime
import numpy as np
PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", ".."))
PROJECT_SOURCE_PATH = os.path.join(PROJECT_ROOT, "source", "rl_training")
if PROJECT_SOURCE_PATH not in sys.path:
sys.path.insert(0, PROJECT_SOURCE_PATH)
from isaaclab.app import AppLauncher
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
import cli_args
parser = argparse.ArgumentParser(description="Evaluate an RSL-RL checkpoint with criterion metrics.")
parser.add_argument("--num_envs", type=int, default=250, help="Number of parallel environments.")
parser.add_argument("--task", type=str, default="Rough-Deeprobotics-M20-v0", 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(
"--eval_type",
type=str,
default="all",
choices=[
"all",
"upper_limits",
"velocity_error",
"access_rate",
"time_consumption",
"energy_consumption",
"motion_stability",
"stability_margin",
"robustness",
],
)
parser.add_argument("--episode_length_s", type=float, default=10.0)
parser.add_argument("--command_x", type=float, default=1.0)
parser.add_argument("--command_y", type=float, default=0.0)
parser.add_argument("--command_yaw", type=float, default=0.0)
parser.add_argument("--terrain_size", type=float, default=2.0, help="Evaluation terrain tile size in meters.")
parser.add_argument("--success_distance", type=float, default=2.0)
parser.add_argument("--start_x", type=float, default=-0.9, help="Evaluation reset x position on the terrain tile.")
parser.add_argument("--start_y", type=float, default=0.0, help="Evaluation reset y position on the terrain tile.")
parser.add_argument("--num_levels", type=int, default=8, help="Number of difficulty levels for upper_limits.")
parser.add_argument("--envs_per_cell", type=int, default=8, help="Number of robots per terrain/difficulty cell.")
parser.add_argument(
"--terrain_type",
type=str,
default="all",
choices=["all", "stairs_up", "stairs_down", "smooth_slope", "rough_slope", "boxes"],
help="Terrain type used by upper_limits.",
)
parser.add_argument("--tilt_threshold", type=float, default=0.70, help="Tilt threshold in radians for success/stability.")
parser.add_argument("--acc_threshold", type=float, default=2.0, help="Base acceleration threshold for instability.")
parser.add_argument("--robot_mass", type=float, default=25.482, help="Robot mass used for COT.")
parser.add_argument("--output", type=str, default=None, help="Optional output JSON path.")
parser.add_argument("--print_json", action="store_true", default=False, help="Print the saved JSON payload to stdout.")
parser.add_argument("--verbose", action="store_true", default=False, help="Print per-terrain per-level debug lines.")
parser.add_argument(
"--include_summary",
action="store_true",
default=False,
help="When eval_type=all, also run a second mixed-terrain rollout for summary metrics.",
)
cli_args.add_rsl_rl_args(parser)
AppLauncher.add_app_launcher_args(parser)
args_cli, hydra_args = parser.parse_known_args()
sys.argv = [sys.argv[0]] + hydra_args
app_launcher = AppLauncher(args_cli)
simulation_app = app_launcher.app
import gymnasium as gym
import torch
from rsl_rl.runners import OnPolicyRunner
from isaaclab.envs import (
DirectMARLEnv,
DirectMARLEnvCfg,
DirectRLEnvCfg,
ManagerBasedRLEnvCfg,
multi_agent_to_single_agent,
)
from isaaclab.terrains.config.rough import ROUGH_TERRAINS_CFG
from isaaclab_rl.rsl_rl import RslRlOnPolicyRunnerCfg, RslRlVecEnvWrapper
from isaaclab_tasks.utils import get_checkpoint_path
from isaaclab_tasks.utils.hydra import hydra_task_config
import rl_training
import rl_training.tasks # noqa: F401
EVAL_TYPES = [
"upper_limits",
"velocity_error",
"access_rate",
"time_consumption",
"energy_consumption",
"motion_stability",
"stability_margin",
"robustness",
]
TERRAIN_LEVELS = {
"stairs_up": [0.04, 0.06, 0.08, 0.10, 0.12, 0.14, 0.16, 0.18],
"stairs_down": [0.04, 0.06, 0.08, 0.10, 0.12, 0.14, 0.16, 0.18],
"smooth_slope": [0.05, 0.10, 0.15, 0.20, 0.25, 0.30, 0.35, 0.40],
"rough_slope": [0.05, 0.10, 0.15, 0.20, 0.25, 0.30, 0.35, 0.40],
"boxes": [0.04, 0.06, 0.08, 0.10, 0.12, 0.14, 0.16, 0.18],
}
TERRAIN_TO_SUBTERRAIN = {
"stairs_up": "pyramid_stairs",
"stairs_down": "pyramid_stairs_inv",
"smooth_slope": "hf_pyramid_slope",
"rough_slope": "random_rough",
"boxes": "boxes",
}
def apply_eval_terrain_baseline(env_cfg) -> None:
"""Use the same evaluation terrain generator for every task."""
if env_cfg.scene.terrain.terrain_type == "plane":
env_cfg.scene.terrain.terrain_type = "generator"
env_cfg.scene.terrain.terrain_generator = copy.deepcopy(ROUGH_TERRAINS_CFG)
env_cfg.scene.terrain.max_init_terrain_level = None
env_cfg.scene.terrain.terrain_generator.size = (args_cli.terrain_size, args_cli.terrain_size)
env_cfg.scene.terrain.terrain_generator.border_width = 2.0
env_cfg.scene.terrain.terrain_generator.curriculum = False
env_cfg.scene.terrain.terrain_generator.num_rows = 5
env_cfg.scene.terrain.terrain_generator.num_cols = 5
env_cfg.scene.terrain.terrain_generator.use_cache = False
for sub_cfg in env_cfg.scene.terrain.terrain_generator.sub_terrains.values():
if hasattr(sub_cfg, "platform_width"):
sub_cfg.platform_width = min(0.4, args_cli.terrain_size * 0.2)
if hasattr(sub_cfg, "border_width"):
sub_cfg.border_width = min(0.1, args_cli.terrain_size * 0.05)
if hasattr(sub_cfg, "step_width"):
sub_cfg.step_width = min(0.25, args_cli.terrain_size * 0.125)
if hasattr(sub_cfg, "grid_width"):
sub_cfg.grid_width = min(0.3, args_cli.terrain_size * 0.15)
def resolve_checkpoint(log_root_path: str, load_run: str | None, checkpoint: str | None) -> str:
if checkpoint is not None:
checkpoint_path = os.path.expanduser(checkpoint)
if os.path.isabs(checkpoint_path) or os.path.dirname(checkpoint_path):
checkpoint_path = os.path.abspath(checkpoint_path)
if not os.path.isfile(checkpoint_path):
raise FileNotFoundError(f"Unable to find checkpoint: {checkpoint_path}")
return checkpoint_path
if load_run is not None:
run_path = os.path.expanduser(load_run)
if os.path.isabs(run_path) or os.path.dirname(run_path):
run_path = os.path.abspath(run_path)
if not os.path.isdir(run_path):
raise FileNotFoundError(f"Unable to find run directory: {run_path}")
return get_checkpoint_path(os.path.dirname(run_path), os.path.basename(run_path), checkpoint)
return get_checkpoint_path(log_root_path, load_run, checkpoint)
def set_fixed_command(env_cfg, command_x: float, command_y: float, command_yaw: float, episode_length_s: float) -> None:
command_cfg = env_cfg.commands.base_velocity
command_cfg.heading_command = False
command_cfg.resampling_time_range = (episode_length_s + 0.01, episode_length_s + 0.01)
command_cfg.ranges.lin_vel_x = (command_x, command_x)
command_cfg.ranges.lin_vel_y = (command_y, command_y)
command_cfg.ranges.ang_vel_z = (command_yaw, command_yaw)
def configure_eval_env(env_cfg, eval_type: str) -> None:
env_cfg.scene.num_envs = args_cli.num_envs
env_cfg.episode_length_s = args_cli.episode_length_s
env_cfg.seed = args_cli.seed if args_cli.seed is not None else 0
env_cfg.sim.device = args_cli.device if args_cli.device is not None else env_cfg.sim.device
env_cfg.observations.policy.enable_corruption = False
set_fixed_command(env_cfg, args_cli.command_x, args_cli.command_y, args_cli.command_yaw, args_cli.episode_length_s)
apply_eval_terrain_baseline(env_cfg)
env_cfg.curriculum.terrain_levels = None
env_cfg.curriculum.command_levels = None
for name in (
"randomize_rigid_body_material",
"randomize_wheel_material",
"randomize_support_material",
"randomize_rigid_body_mass",
"randomize_rigid_body_mass_base",
"randomize_rigid_body_inertia",
"randomize_com_positions",
"randomize_apply_external_force_torque",
"randomize_actuator_gains",
):
if hasattr(env_cfg.events, name):
setattr(env_cfg.events, name, None)
if hasattr(env_cfg.events, "randomize_reset_base") and env_cfg.events.randomize_reset_base is not None:
env_cfg.events.randomize_reset_base.params = {
"pose_range": {
"x": (args_cli.start_x, args_cli.start_x),
"y": (args_cli.start_y, args_cli.start_y),
"z": (0.0, 0.0),
"roll": (0.0, 0.0),
"pitch": (0.0, 0.0),
"yaw": (0.0, 0.0),
},
"velocity_range": {
"x": (0.0, 0.0),
"y": (0.0, 0.0),
"z": (0.0, 0.0),
"roll": (0.0, 0.0),
"pitch": (0.0, 0.0),
"yaw": (0.0, 0.0),
},
}
if hasattr(env_cfg.events, "randomize_push_robot"):
env_cfg.events.randomize_push_robot = None
if eval_type == "robustness" and hasattr(env_cfg.events, "randomize_push_robot"):
# Keep robustness deterministic enough to compare runs, but enable interval pushes.
from isaaclab.managers import EventTermCfg as EventTerm
import rl_training.tasks.manager_based.locomotion.velocity.mdp as mdp
env_cfg.events.randomize_push_robot = EventTerm(
func=mdp.push_by_setting_velocity,
mode="interval",
interval_range_s=(2.0, 2.01),
params={"velocity_range": {"x": (-0.5, 0.5), "y": (-0.5, 0.5)}},
)
def configure_upper_limit_env(env_cfg, terrain_types: list[str]) -> None:
"""Configure one fixed terrain grid: rows are difficulty levels, columns are terrain types."""
configure_eval_env(env_cfg, "all")
terrain_generator = env_cfg.scene.terrain.terrain_generator
terrain_generator.curriculum = True
terrain_generator.num_rows = args_cli.num_levels
terrain_generator.num_cols = len(terrain_types)
terrain_generator.difficulty_range = (0.0, 1.0)
if hasattr(terrain_generator, "seed"):
terrain_generator.seed = args_cli.seed if args_cli.seed is not None else 0
env_cfg.scene.terrain.max_init_terrain_level = args_cli.num_levels - 1
env_cfg.scene.num_envs = args_cli.num_levels * len(terrain_types) * args_cli.envs_per_cell
selected_subterrains = [TERRAIN_TO_SUBTERRAIN[terrain_type] for terrain_type in terrain_types]
original_subterrains = terrain_generator.sub_terrains
terrain_generator.sub_terrains = {
subterrain_name: copy.deepcopy(original_subterrains[subterrain_name]) for subterrain_name in selected_subterrains
}
for name, sub_cfg in terrain_generator.sub_terrains.items():
sub_cfg.proportion = selected_subterrains.count(name) / len(selected_subterrains)
for terrain_type in terrain_types:
sub_cfg = terrain_generator.sub_terrains[TERRAIN_TO_SUBTERRAIN[terrain_type]]
levels = TERRAIN_LEVELS[terrain_type][: args_cli.num_levels]
if terrain_type in ("stairs_up", "stairs_down"):
sub_cfg.step_height_range = (levels[0], levels[-1])
elif terrain_type == "smooth_slope":
sub_cfg.slope_range = (levels[0], levels[-1])
elif terrain_type == "rough_slope":
sub_cfg.noise_range = (levels[0], levels[-1])
sub_cfg.noise_step = 0.01
elif terrain_type == "boxes":
sub_cfg.grid_height_range = (levels[0], levels[-1])
def assign_upper_limit_origins(env, terrain_types: list[str]) -> tuple[torch.Tensor, torch.Tensor]:
"""Assign an equal number of robots to every terrain/difficulty cell."""
base_env = env.unwrapped
terrain = base_env.scene.terrain
device = base_env.device
num_levels = args_cli.num_levels
num_types = len(terrain_types)
levels = []
types = []
for terrain_type_id in range(num_types):
for level_id in range(num_levels):
levels.extend([level_id] * args_cli.envs_per_cell)
types.extend([terrain_type_id] * args_cli.envs_per_cell)
levels_tensor = torch.tensor(levels, dtype=torch.long, device=device)
types_tensor = torch.tensor(types, dtype=torch.long, device=device)
terrain.terrain_levels = levels_tensor
terrain.terrain_types = types_tensor
terrain.env_origins = terrain.terrain_origins[levels_tensor, types_tensor]
env.reset()
return levels_tensor, types_tensor
def tilt_from_projected_gravity(projected_gravity_b: torch.Tensor) -> torch.Tensor:
z_axis = torch.clamp(-projected_gravity_b[:, 2], min=-1.0, max=1.0)
return torch.acos(z_axis)
def support_margin_xy(root_xy: torch.Tensor, foot_xy: torch.Tensor) -> torch.Tensor:
min_xy = torch.min(foot_xy, dim=1).values
max_xy = torch.max(foot_xy, dim=1).values
inside_x = (root_xy[:, 0] >= min_xy[:, 0]) & (root_xy[:, 0] <= max_xy[:, 0])
inside_y = (root_xy[:, 1] >= min_xy[:, 1]) & (root_xy[:, 1] <= max_xy[:, 1])
inside = inside_x & inside_y
inside_dist = torch.minimum(
torch.minimum(root_xy[:, 0] - min_xy[:, 0], max_xy[:, 0] - root_xy[:, 0]),
torch.minimum(root_xy[:, 1] - min_xy[:, 1], max_xy[:, 1] - root_xy[:, 1]),
)
outside_dx = torch.clamp(torch.maximum(min_xy[:, 0] - root_xy[:, 0], root_xy[:, 0] - max_xy[:, 0]), min=0.0)
outside_dy = torch.clamp(torch.maximum(min_xy[:, 1] - root_xy[:, 1], root_xy[:, 1] - max_xy[:, 1]), min=0.0)
outside_dist = torch.sqrt(outside_dx**2 + outside_dy**2)
return torch.where(inside, inside_dist, -outside_dist)
def run_rollout(env, policy, include_per_env: bool = False) -> dict[str, object]:
base_env = env.unwrapped
robot = base_env.scene["robot"]
dt = base_env.step_dt
device = base_env.device
max_steps = int(base_env.max_episode_length)
command = torch.tensor([args_cli.command_x, args_cli.command_y, args_cli.command_yaw], device=device)
command_xy = command[:2].unsqueeze(0)
command_yaw = command[2]
command_dir = torch.tensor([args_cli.command_x, args_cli.command_y], device=device)
if torch.linalg.norm(command_dir) < 1e-6:
command_dir = torch.tensor([1.0, 0.0], device=device)
command_dir = command_dir / torch.linalg.norm(command_dir)
obs = env.get_observations()
start_pos = robot.data.root_pos_w[:, :3].clone()
last_lin_vel = robot.data.root_lin_vel_w[:, :3].clone()
complete_time = torch.full((base_env.num_envs,), float("nan"), device=device)
energy = torch.zeros(base_env.num_envs, device=device)
route = torch.zeros(base_env.num_envs, device=device)
lin_error_sum = torch.zeros(base_env.num_envs, device=device)
yaw_error_sum = torch.zeros(base_env.num_envs, device=device)
instability_count = torch.zeros(base_env.num_envs, device=device)
max_tilt = torch.zeros(base_env.num_envs, device=device)
stable_time = torch.zeros(base_env.num_envs, device=device)
margin_sum = torch.zeros(base_env.num_envs, device=device)
margin_count = 0
max_progress = torch.zeros(base_env.num_envs, device=device)
reached = torch.zeros(base_env.num_envs, dtype=torch.bool, device=device)
ever_fallen = torch.zeros(base_env.num_envs, dtype=torch.bool, device=device)
support_steps = torch.zeros(base_env.num_envs, device=device)
support_events = torch.zeros(base_env.num_envs, device=device)
max_support_time = torch.zeros(base_env.num_envs, device=device)
previous_support_active = torch.zeros(base_env.num_envs, dtype=torch.bool, device=device)
wheel_body_ids, _ = robot.find_bodies(".*_wheel")
contact_sensor = base_env.scene.sensors["contact_forces"]
support_body_ids, _ = contact_sensor.find_bodies(["base_link", ".*_knee"])
for step in range(max_steps):
pre_progress = torch.sum((robot.data.root_pos_w[:, :2] - start_pos[:, :2]) * command_dir.unsqueeze(0), dim=1)
max_progress = torch.maximum(max_progress, pre_progress)
reached |= pre_progress >= args_cli.success_distance
with torch.inference_mode():
actions = policy(obs)
if isinstance(actions, tuple):
actions = actions[0]
obs, _, dones, _ = env.step(actions)
lin_vel_b = robot.data.root_lin_vel_b[:, :2]
yaw_vel_b = robot.data.root_ang_vel_b[:, 2]
lin_error_sum += torch.linalg.norm(lin_vel_b - command_xy, dim=1)
yaw_error_sum += torch.abs(yaw_vel_b - command_yaw)
joint_power = torch.abs(robot.data.applied_torque * robot.data.joint_vel)
energy += torch.sum(joint_power, dim=1) * dt
route += torch.linalg.norm(robot.data.root_lin_vel_w[:, :2], dim=1) * dt
acc = torch.linalg.norm(robot.data.root_lin_vel_w[:, :3] - last_lin_vel, dim=1) / dt
instability_count += (acc > args_cli.acc_threshold).float()
last_lin_vel = robot.data.root_lin_vel_w[:, :3].clone()
tilt = tilt_from_projected_gravity(robot.data.projected_gravity_b)
max_tilt = torch.maximum(max_tilt, tilt)
stable_time += (tilt < args_cli.tilt_threshold).float() * dt
progress = torch.sum((robot.data.root_pos_w[:, :2] - start_pos[:, :2]) * command_dir.unsqueeze(0), dim=1)
max_progress = torch.maximum(max_progress, progress)
reached |= progress >= args_cli.success_distance
newly_success = torch.isnan(complete_time) & (progress >= args_cli.success_distance)
complete_time[newly_success] = (step + 1) * dt
ever_fallen |= tilt > args_cli.tilt_threshold
support_force = torch.linalg.norm(
contact_sensor.data.net_forces_w[:, support_body_ids, :], dim=-1
)
support_time = contact_sensor.data.current_contact_time[:, support_body_ids]
support_active = torch.any(support_force > 15.0, dim=1)
support_steps += support_active.float()
support_events += (support_active & ~previous_support_active).float()
max_support_time = torch.maximum(max_support_time, torch.amax(support_time, dim=1))
previous_support_active = support_active
if len(wheel_body_ids) >= 3:
foot_xy = robot.data.body_pos_w[:, wheel_body_ids, :2]
margin_sum += support_margin_xy(robot.data.root_pos_w[:, :2], foot_xy)
margin_count += 1
tilt = tilt_from_projected_gravity(robot.data.projected_gravity_b)
success = reached & ~ever_fallen
success_count = torch.sum(success).item()
success_den = max(base_env.num_envs, 1)
successful_time = complete_time[success & torch.isfinite(complete_time)]
avg_time = torch.mean(successful_time).item() if successful_time.numel() > 0 else float("nan")
cot = energy / ((route + 1e-6) * args_cli.robot_mass * 9.81)
metrics = {
"num_envs": float(base_env.num_envs),
"episode_length_s": float(args_cli.episode_length_s),
"terrain_size_m": float(args_cli.terrain_size),
"success_distance_m": float(args_cli.success_distance),
"success_count": float(success_count),
"success_rate": float(success_count / success_den),
"avg_final_progress_m": float(torch.mean(max_progress).item()),
"avg_time_consumption_s": float(avg_time),
"time_efficiency": float(avg_time / (args_cli.success_distance / max(abs(args_cli.command_x), 1e-6)))
if successful_time.numel() > 0
else float("nan"),
"mean_lin_vel_error": float(torch.mean(lin_error_sum / max_steps).item()),
"mean_yaw_vel_error": float(torch.mean(yaw_error_sum / max_steps).item()),
"avg_energy_j": float(torch.mean(energy).item()),
"avg_cost_of_transport": float(torch.mean(cot).item()),
"unstability_rate": float(torch.mean(instability_count / max_steps).item()),
"avg_stability_margin_m": float(torch.mean(margin_sum / max(margin_count, 1)).item()),
"avg_max_tilt_rad": float(torch.mean(max_tilt).item()),
"avg_stable_time_s": float(torch.mean(stable_time).item()),
"avg_support_contact_ratio": float(torch.mean(support_steps / max_steps).item()),
"avg_support_events": float(torch.mean(support_events).item()),
"avg_max_support_contact_time_s": float(torch.mean(max_support_time).item()),
"successful_runs_with_support_ratio": float(
torch.mean((support_events[success] > 0.0).float()).item()
)
if success_count > 0
else float("nan"),
}
if include_per_env:
metrics["_per_env"] = {
"success": success.detach().cpu().tolist(),
"max_progress": max_progress.detach().cpu().tolist(),
"max_tilt": max_tilt.detach().cpu().tolist(),
}
return metrics
def make_env_and_policy(env_cfg, agent_cfg, resume_path: str):
env = gym.make(args_cli.task, cfg=env_cfg)
if isinstance(env.unwrapped, DirectMARLEnv):
env = multi_agent_to_single_agent(env)
env = RslRlVecEnvWrapper(env, clip_actions=agent_cfg.clip_actions)
runner = OnPolicyRunner(env, agent_cfg.to_dict(), log_dir=None, device=agent_cfg.device)
runner.load(resume_path)
policy = runner.get_inference_policy(device=env.unwrapped.device)
return env, policy
def print_upper_limit_matrix(title: str, terrain_types: list[str], matrix: list[list[float | None]]) -> None:
"""Print an upper-limit success matrix with levels as rows and terrain types as columns."""
col_width = 14
print(f"\n{title}")
print("level".ljust(8) + "".join(terrain_type[: col_width - 1].rjust(col_width) for terrain_type in terrain_types))
for level_index, row in enumerate(matrix, start=1):
cells = []
for value in row:
cells.append(" -".rjust(col_width) if value is None else f"{value:.2f}".rjust(col_width))
print(f"{level_index:<8}" + "".join(cells))
def matrix_to_numpy(matrix: list[list[float | None]]) -> np.ndarray:
return np.array([[np.nan if value is None else value for value in row] for row in matrix], dtype=np.float32)
def print_numpy_matrix(title: str, matrix: list[list[float | None]]) -> None:
print(f"{title}:")
print(np.array2string(matrix_to_numpy(matrix).round(2), precision=2, suppress_small=True))
def print_upper_limit_summary(result: dict[str, object], terrain_types: list[str]) -> None:
matrix = result["_matrix"]
print_numpy_matrix("success rate", matrix["success_rate"])
print_numpy_matrix("success rate accumulated", matrix["success_rate_accumulated"])
score = np.array([result[terrain_type]["score"] for terrain_type in terrain_types], dtype=np.float32)
print(f"score: {np.array2string(score.round(2), precision=2, suppress_small=True)} of {len(terrain_types)}")
def print_rollout_summary(metrics: dict[str, object]) -> None:
success_rate = float(metrics["success_rate"])
success_count = int(round(float(metrics["success_count"])))
avg_time = float(metrics["avg_time_consumption_s"])
avg_time_text = "nan" if np.isnan(avg_time) else f"{avg_time:.2f}"
print(f"success rate: {success_rate:.2f}")
print(f"success count: {success_count} time consumption: {avg_time_text}")
print(f"average power consumption: {float(metrics['avg_energy_j']):.2f}")
print(f"average cost of transport: {float(metrics['avg_cost_of_transport']):.2f}")
print(f"unstability rate: {float(metrics['unstability_rate']) * 100:.1f}%")
print(f"average stability margin: {float(metrics['avg_stability_margin_m']):.2f}")
print(
f"average max tilt: {float(metrics['avg_max_tilt_rad']):.2f} "
f"average max time: {float(metrics['avg_stable_time_s']):.2f}"
)
support_success_ratio = float(metrics["successful_runs_with_support_ratio"])
support_success_text = "nan" if np.isnan(support_success_ratio) else f"{support_success_ratio * 100:.1f}%"
print(
f"support contact ratio: {float(metrics['avg_support_contact_ratio']) * 100:.1f}% "
f"events/episode: {float(metrics['avg_support_events']):.2f} "
f"max duration: {float(metrics['avg_max_support_contact_time_s']):.2f}s "
f"successful runs using support: {support_success_text}"
)
def print_metric_result(eval_type: str, metrics: dict[str, object]) -> None:
if eval_type == "velocity_error":
print(f"mean lin vel error: {float(metrics['mean_lin_vel_error']):.2f}")
print(f"mean yaw vel error: {float(metrics['mean_yaw_vel_error']):.2f}")
elif eval_type == "access_rate":
print(f"success rate: {float(metrics['success_rate']):.2f}")
elif eval_type == "time_consumption":
success_count = int(round(float(metrics["success_count"])))
avg_time = float(metrics["avg_time_consumption_s"])
avg_time_text = "nan" if np.isnan(avg_time) else f"{avg_time:.2f}"
print(f"success count: {success_count} time consumption: {avg_time_text}")
elif eval_type == "energy_consumption":
print(f"average power consumption: {float(metrics['avg_energy_j']):.2f}")
print(f"average cost of transport: {float(metrics['avg_cost_of_transport']):.2f}")
elif eval_type == "motion_stability":
print(f"unstability rate: {float(metrics['unstability_rate']) * 100:.1f}%")
elif eval_type == "stability_margin":
print(f"average stability margin: {float(metrics['avg_stability_margin_m']):.2f}")
elif eval_type == "robustness":
print(
f"average max tilt: {float(metrics['avg_max_tilt_rad']):.2f} "
f"average max time: {float(metrics['avg_stable_time_s']):.2f}"
)
def run_upper_limits(env_cfg, agent_cfg, resume_path: str) -> dict[str, object]:
terrain_types = list(TERRAIN_LEVELS) if args_cli.terrain_type == "all" else [args_cli.terrain_type]
eval_cfg = copy.deepcopy(env_cfg)
configure_upper_limit_env(eval_cfg, terrain_types)
env, policy = make_env_and_policy(eval_cfg, agent_cfg, resume_path)
level_ids, type_ids = assign_upper_limit_origins(env, terrain_types)
metrics = run_rollout(env, policy, include_per_env=True)
env.close()
per_env = metrics.pop("_per_env")
success = torch.tensor(per_env["success"], dtype=torch.float32)
max_progress = torch.tensor(per_env["max_progress"], dtype=torch.float32)
max_tilt = torch.tensor(per_env["max_tilt"], dtype=torch.float32)
level_ids_cpu = level_ids.cpu()
type_ids_cpu = type_ids.cpu()
result: dict[str, object] = {}
for terrain_type_id, terrain_type in enumerate(terrain_types):
level_values = TERRAIN_LEVELS[terrain_type][: args_cli.num_levels]
rates = []
level_metrics = []
for level_index, level_value in enumerate(level_values):
mask = (type_ids_cpu == terrain_type_id) & (level_ids_cpu == level_index)
success_count = float(torch.sum(success[mask]).item())
total_count = int(torch.sum(mask).item())
success_rate = success_count / max(total_count, 1)
avg_progress = float(torch.mean(max_progress[mask]).item()) if total_count > 0 else float("nan")
avg_tilt = float(torch.mean(max_tilt[mask]).item()) if total_count > 0 else float("nan")
rates.append(success_rate)
level_metrics.append(
{
"level": level_index + 1,
"difficulty": level_value,
"success_rate": success_rate,
"success_count": success_count,
"num_envs": total_count,
"avg_progress_m": avg_progress,
"avg_max_tilt_rad": avg_tilt,
}
)
if args_cli.verbose:
print(
f"[EVAL] {terrain_type} level={level_index + 1} difficulty={level_value:.3f} "
f"success_rate={success_rate:.3f}"
)
accumulated = []
running = 1.0
for rate in rates:
running *= rate
accumulated.append(running)
score = 0.0
for idx, acc_rate in enumerate(accumulated):
next_fail = 1.0 - rates[idx + 1] if idx + 1 < len(rates) else 1.0
score += acc_rate * (idx + 1) * next_fail
result[terrain_type] = {
"levels": level_metrics,
"success_rate": rates,
"success_rate_accumulated": accumulated,
"score": score,
"max_level": len(level_values),
"benchmark": "fixed_rough_terrain_v1",
"terrain_size_m": args_cli.terrain_size,
"success_distance_m": args_cli.success_distance,
"episode_length_s": args_cli.episode_length_s,
}
max_levels = max((len(result[terrain_type]["success_rate"]) for terrain_type in terrain_types), default=0)
success_matrix: list[list[float | None]] = []
accumulated_matrix: list[list[float | None]] = []
difficulty_matrix: list[list[float | None]] = []
for level_index in range(max_levels):
success_row = []
accumulated_row = []
difficulty_row = []
for terrain_type in terrain_types:
rates = result[terrain_type]["success_rate"]
accumulated = result[terrain_type]["success_rate_accumulated"]
levels = result[terrain_type]["levels"]
success_row.append(rates[level_index] if level_index < len(rates) else None)
accumulated_row.append(accumulated[level_index] if level_index < len(accumulated) else None)
difficulty_row.append(levels[level_index]["difficulty"] if level_index < len(levels) else None)
success_matrix.append(success_row)
accumulated_matrix.append(accumulated_row)
difficulty_matrix.append(difficulty_row)
result["_matrix"] = {
"terrain_types": terrain_types,
"success_rate": success_matrix,
"success_rate_accumulated": accumulated_matrix,
"difficulty": difficulty_matrix,
"terrain_size_m": args_cli.terrain_size,
"success_distance_m": args_cli.success_distance,
"episode_length_s": args_cli.episode_length_s,
}
result["_summary"] = metrics
print_upper_limit_summary(result, terrain_types)
return result
def filter_metrics(metrics: dict[str, float], eval_type: str) -> dict[str, float]:
if eval_type == "all":
return metrics
keys = {
"velocity_error": ("mean_lin_vel_error", "mean_yaw_vel_error"),
"access_rate": ("success_count", "success_rate", "avg_final_progress_m"),
"time_consumption": ("success_count", "avg_time_consumption_s", "time_efficiency"),
"energy_consumption": ("avg_energy_j", "avg_cost_of_transport"),
"motion_stability": ("unstability_rate",),
"stability_margin": ("avg_stability_margin_m",),
"robustness": ("success_rate", "avg_max_tilt_rad", "avg_stable_time_s"),
}[eval_type]
return {key: metrics[key] for key in keys}
@hydra_task_config(args_cli.task, args_cli.agent)
def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agent_cfg: RslRlOnPolicyRunnerCfg):
agent_cfg = cli_args.update_rsl_rl_cfg(agent_cfg, args_cli)
log_root_path = os.path.abspath(os.path.join("logs", "rsl_rl", agent_cfg.experiment_name))
resume_path = resolve_checkpoint(log_root_path, agent_cfg.load_run, agent_cfg.load_checkpoint)
print(f"[INFO] Evaluating checkpoint: {resume_path}")
if args_cli.eval_type == "upper_limits":
selected = run_upper_limits(env_cfg, agent_cfg, resume_path)
elif args_cli.eval_type == "all":
upper_limits = run_upper_limits(env_cfg, agent_cfg, resume_path)
selected = {"upper_limits": upper_limits}
print_rollout_summary(upper_limits["_summary"])
if args_cli.include_summary:
summary_cfg = copy.deepcopy(env_cfg)
configure_eval_env(summary_cfg, "all")
env, policy = make_env_and_policy(summary_cfg, agent_cfg, resume_path)
summary = run_rollout(env, policy)
env.close()
selected["summary"] = summary
else:
eval_type_for_env = "robustness" if args_cli.eval_type == "robustness" else "all"
configure_eval_env(env_cfg, eval_type_for_env)
env, policy = make_env_and_policy(env_cfg, agent_cfg, resume_path)
metrics = run_rollout(env, policy)
selected = filter_metrics(metrics, args_cli.eval_type)
env.close()
print_metric_result(args_cli.eval_type, selected)
if args_cli.print_json:
print(json.dumps(selected, indent=2, sort_keys=True))
output = args_cli.output
if output is None:
eval_dir = os.path.join(os.path.dirname(resume_path), "eval")
os.makedirs(eval_dir, exist_ok=True)
suffix = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
output = os.path.join(eval_dir, f"{args_cli.eval_type}_{suffix}.json")
else:
output = os.path.abspath(os.path.expanduser(output))
output_dir = os.path.dirname(output)
if output_dir:
os.makedirs(output_dir, exist_ok=True)
with open(output, "w", encoding="utf-8") as f:
json.dump({"eval_type": args_cli.eval_type, "checkpoint": resume_path, "metrics": selected}, f, indent=2)
print(f"[INFO] Saved evaluation result to: {output}")
if __name__ == "__main__":
main()
simulation_app.close()
+23
View File
@@ -0,0 +1,23 @@
#!/usr/bin/env bash
set -euo pipefail
TASK=${TASK:-Rough-Deeprobotics-M20-v0}
CHECKPOINT=${1:-${CHECKPOINT:-}}
NUM_ENVS=${NUM_ENVS:-250}
DEVICE=${DEVICE:-cuda:0}
EVAL_TYPE=${EVAL_TYPE:-all}
if [[ -z "${CHECKPOINT}" ]]; then
echo "Usage: $0 <checkpoint.pt>"
echo "Or set CHECKPOINT=/path/to/model.pt"
exit 1
fi
echo "Running eval_type=${EVAL_TYPE}"
python scripts/reinforcement_learning/rsl_rl/eval.py \
--task="${TASK}" \
--checkpoint="${CHECKPOINT}" \
--num_envs="${NUM_ENVS}" \
--device="${DEVICE}" \
--eval_type="${EVAL_TYPE}" \
--headless
@@ -0,0 +1,280 @@
# Copyright (c) 2025 Deep Robotics
# SPDX-License-Identifier: BSD 3-Clause
# Copyright (c) 2024-2025 Ziqi Fan
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) 2024-2025, The Isaac Lab Project Developers.
# All rights reserved.
#
# SPDX-License-Identifier: Apache-2.0
"""Script to play a checkpoint if an RL agent from RSL-RL."""
"""Launch Isaac Sim Simulator first."""
import argparse
import os
import sys
PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", ".."))
PROJECT_SOURCE_PATH = os.path.join(PROJECT_ROOT, "source", "rl_training")
if PROJECT_SOURCE_PATH not in sys.path:
sys.path.insert(0, PROJECT_SOURCE_PATH)
from isaaclab.app import AppLauncher
# local imports
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
import cli_args
from rl_utils import camera_follow
# add argparse arguments
parser = argparse.ArgumentParser(description="Train an RL agent with RSL-RL.")
parser.add_argument("--video", action="store_true", default=False, help="Record videos during training.")
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("--keyboard", action="store_true", default=False, help="Whether to use keyboard.")
parser.add_argument(
"--visual-only",
action="store_true",
default=False,
help="Display the environment with zero actions without loading an RL checkpoint.",
)
# append RSL-RL cli arguments
cli_args.add_rsl_rl_args(parser)
# append AppLauncher cli args
AppLauncher.add_app_launcher_args(parser)
# parse the arguments
args_cli, hydra_args = parser.parse_known_args()
# always enable cameras to record video
if args_cli.video:
args_cli.enable_cameras = True
# clear out sys.argv for Hydra
sys.argv = [sys.argv[0]] + hydra_args
# launch omniverse app
app_launcher = AppLauncher(args_cli)
simulation_app = app_launcher.app
"""Rest everything follows."""
import gymnasium as gym
import time
import torch
from rsl_rl.runners import OnPolicyRunner
from isaaclab.devices import Se2Keyboard, Se2KeyboardCfg
from isaaclab.envs import (
DirectMARLEnv,
DirectMARLEnvCfg,
DirectRLEnvCfg,
ManagerBasedRLEnvCfg,
multi_agent_to_single_agent,
)
from isaaclab.managers import ObservationTermCfg as ObsTerm
from isaaclab.utils.assets import retrieve_file_path
from isaaclab.utils.dict import print_dict
from isaaclab.utils.pretrained_checkpoint import get_published_pretrained_checkpoint
from isaaclab_rl.rsl_rl import RslRlOnPolicyRunnerCfg, RslRlVecEnvWrapper, export_policy_as_jit, export_policy_as_onnx
from isaaclab_tasks.utils import get_checkpoint_path
from isaaclab_tasks.utils.hydra import hydra_task_config
import rl_training
import rl_training.tasks # noqa: F401
print(f"[INFO] Using rl_training package from: {rl_training.__file__}")
@hydra_task_config(args_cli.task, args_cli.agent)
def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agent_cfg: RslRlOnPolicyRunnerCfg):
"""Play with RSL-RL agent."""
task_name = args_cli.task.split(":")[-1]
# 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 50
# 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
# spawn the robot randomly in the grid (instead of their terrain levels)
env_cfg.scene.terrain.max_init_terrain_level = None
# reduce the number of terrains to save memory
if env_cfg.scene.terrain.terrain_generator is not None:
env_cfg.scene.terrain.terrain_generator.num_rows = 5
env_cfg.scene.terrain.terrain_generator.num_cols = 5
env_cfg.scene.terrain.terrain_generator.curriculum = False
# disable randomization for play
env_cfg.observations.policy.enable_corruption = False
# Remove perturbations and parameter randomization for reproducible playback.
for event_name in (
"randomize_rigid_body_material",
"randomize_wheel_material",
"randomize_support_material",
"randomize_rigid_body_mass",
"randomize_rigid_body_mass_base",
"randomize_rigid_body_inertia",
"randomize_com_positions",
"randomize_apply_external_force_torque",
"randomize_actuator_gains",
"randomize_push_robot",
):
if hasattr(env_cfg.events, event_name):
setattr(env_cfg.events, event_name, None)
env_cfg.curriculum.command_levels = None
if args_cli.keyboard:
env_cfg.scene.num_envs = 1
env_cfg.terminations.time_out = None
env_cfg.commands.base_velocity.debug_vis = False
config = Se2KeyboardCfg(
v_x_sensitivity=env_cfg.commands.base_velocity.ranges.lin_vel_x[1]/2,
v_y_sensitivity=env_cfg.commands.base_velocity.ranges.lin_vel_y[1],
omega_z_sensitivity=env_cfg.commands.base_velocity.ranges.ang_vel_z[1],
)
controller = Se2Keyboard(config)
env_cfg.observations.policy.velocity_commands = ObsTerm(
func=lambda env: torch.tensor(controller.advance(), dtype=torch.float32).unsqueeze(0).to(env.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)
if args_cli.visual_only:
resume_path = None
log_dir = os.path.join(log_root_path, "visual_only")
print("[INFO] Visual-only mode: no policy checkpoint will be loaded.")
else:
print(f"[INFO] Loading experiment from directory: {log_root_path}")
if args_cli.use_pretrained_checkpoint:
resume_path = get_published_pretrained_checkpoint("rsl_rl", task_name)
if not resume_path:
print("[INFO] Unfortunately a pre-trained checkpoint is currently unavailable for this task.")
return
elif args_cli.load_run is not None:
resume_path = get_checkpoint_path(log_root_path, agent_cfg.load_run, agent_cfg.load_checkpoint)
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)
# 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, DirectMARLEnv):
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)
if args_cli.visual_only:
policy = None
else:
print(f"[INFO]: Loading model checkpoint from: {resume_path}")
# load previously trained model
ppo_runner = OnPolicyRunner(env, agent_cfg.to_dict(), log_dir=None, device=agent_cfg.device)
ppo_runner.load(resume_path)
# obtain the trained policy for inference
policy = ppo_runner.get_inference_policy(device=env.unwrapped.device)
# extract the neural network module
# we do this in a try-except to maintain backwards compatibility.
try:
# version 2.3 onwards
policy_nn = ppo_runner.alg.policy
except AttributeError:
# version 2.2 and below
policy_nn = ppo_runner.alg.actor_critic
# export policy to onnx/jit
export_model_dir = os.path.join(os.path.dirname(resume_path), "exported")
export_policy_as_onnx(
policy=policy_nn,
normalizer=None,
path=export_model_dir,
filename="policy.onnx",
)
export_policy_as_jit(
policy=policy_nn,
normalizer=None,
path=export_model_dir,
filename="policy.pt",
)
dt = env.unwrapped.step_dt
# print(dt, "dt")
# reset environment
obs = env.get_observations()
timestep = 0
# simulate environment
while simulation_app.is_running():
start_time = time.time()
# run everything in inference mode
with torch.inference_mode():
# agent stepping
if policy is None:
actions = torch.zeros((env.num_envs, env.num_actions), device=env.unwrapped.device)
else:
actions = policy(obs)
# env stepping
obs, _, _, _ = env.step(actions)
if args_cli.video:
timestep += 1
# Exit the play loop after recording one video
if timestep == args_cli.video_length:
break
if args_cli.keyboard:
camera_follow(env)
# time delay for real-time evaluation
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()
if __name__ == "__main__":
# run the main function
main()
# close sim app
simulation_app.close()
@@ -0,0 +1,214 @@
# Copyright (c) 2025 Deep Robotics
# SPDX-License-Identifier: BSD 3-Clause
# Copyright (c) 2024-2025 Ziqi Fan
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) 2024-2025, The Isaac Lab Project Developers.
# All rights reserved.
#
# SPDX-License-Identifier: Apache-2.0
"""Script to train RL agent with RSL-RL."""
"""Launch Isaac Sim Simulator first."""
import argparse
import os
import sys
PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", ".."))
PROJECT_SOURCE_PATH = os.path.join(PROJECT_ROOT, "source", "rl_training")
if PROJECT_SOURCE_PATH not in sys.path:
sys.path.insert(0, PROJECT_SOURCE_PATH)
from isaaclab.app import AppLauncher
# local imports
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
import cli_args
# add argparse arguments
parser = argparse.ArgumentParser(description="Train an RL agent with RSL-RL.")
parser.add_argument("--video", action="store_true", default=False, help="Record videos during training.")
parser.add_argument("--video_length", type=int, default=200, help="Length of the recorded video (in steps).")
parser.add_argument("--video_interval", type=int, default=2000, help="Interval between video recordings (in steps).")
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("--max_iterations", type=int, default=None, help="RL Policy training iterations.")
parser.add_argument(
"--distributed", action="store_true", default=False, help="Run training with multiple GPUs or nodes."
)
# append RSL-RL cli arguments
cli_args.add_rsl_rl_args(parser)
# append AppLauncher cli args
AppLauncher.add_app_launcher_args(parser)
args_cli, hydra_args = parser.parse_known_args()
# always enable cameras to record video
if args_cli.video:
args_cli.enable_cameras = True
# clear out sys.argv for Hydra
sys.argv = [sys.argv[0]] + hydra_args
# launch omniverse app
app_launcher = AppLauncher(args_cli)
simulation_app = app_launcher.app
"""Rest everything follows."""
import gymnasium as gym
import os
import torch
from datetime import datetime
from rsl_rl.runners import OnPolicyRunner
from isaaclab.envs import (
DirectMARLEnv,
DirectMARLEnvCfg,
DirectRLEnvCfg,
ManagerBasedRLEnvCfg,
multi_agent_to_single_agent,
)
from isaaclab.utils.dict import print_dict
from isaaclab.utils.io import dump_yaml
from isaaclab_rl.rsl_rl import RslRlOnPolicyRunnerCfg, RslRlVecEnvWrapper
from isaaclab_tasks.utils import get_checkpoint_path
from isaaclab_tasks.utils.hydra import hydra_task_config
import rl_training
import rl_training.tasks # noqa: F401
print(f"[INFO] Using rl_training package from: {rl_training.__file__}")
torch.backends.cuda.matmul.allow_tf32 = True
torch.backends.cudnn.allow_tf32 = True
torch.backends.cudnn.deterministic = False
torch.backends.cudnn.benchmark = False
def resolve_resume_path(log_root_path: str, load_run: str | None, load_checkpoint: str | None) -> str:
"""Resolve a resume checkpoint, allowing explicit paths across experiment folders."""
if load_checkpoint is not None:
checkpoint_path = os.path.expanduser(load_checkpoint)
if os.path.isabs(checkpoint_path) or os.path.dirname(checkpoint_path):
checkpoint_path = os.path.abspath(checkpoint_path)
if not os.path.isfile(checkpoint_path):
raise FileNotFoundError(f"Unable to find the checkpoint file: {checkpoint_path}")
return checkpoint_path
if load_run is not None:
run_path = os.path.expanduser(load_run)
if os.path.isabs(run_path) or os.path.dirname(run_path):
run_path = os.path.abspath(run_path)
if not os.path.isdir(run_path):
raise FileNotFoundError(f"Unable to find the run directory: {run_path}")
return get_checkpoint_path(os.path.dirname(run_path), os.path.basename(run_path), load_checkpoint)
return get_checkpoint_path(log_root_path, load_run, load_checkpoint)
@hydra_task_config(args_cli.task, args_cli.agent)
def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agent_cfg: RslRlOnPolicyRunnerCfg):
"""Train with RSL-RL agent."""
# 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
agent_cfg.max_iterations = (
args_cli.max_iterations if args_cli.max_iterations is not None else agent_cfg.max_iterations
)
# 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
for reward_name in (
"track_diff_wheel_yaw_exp",
"lin_vel_xy_yaw_command_l2",
"hipx_joint_pos_penalty",
"hipy_joint_pos_penalty",
):
reward_term = getattr(env_cfg.rewards, reward_name, None)
reward_weight = None if reward_term is None else reward_term.weight
print(f"[INFO] Reward term {reward_name}: weight={reward_weight}")
# multi-gpu training configuration
if args_cli.distributed:
env_cfg.sim.device = f"cuda:{app_launcher.local_rank}"
agent_cfg.device = f"cuda:{app_launcher.local_rank}"
# set seed to have diversity in different threads
seed = agent_cfg.seed + app_launcher.local_rank
env_cfg.seed = seed
agent_cfg.seed = seed
# 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] Logging experiment in directory: {log_root_path}")
# specify directory for logging runs: {time-stamp}_{run_name}
log_dir = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
# The Ray Tune workflow extracts experiment name using the logging line below, hence, do not change it (see PR #2346, comment-2819298849)
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)
# 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, DirectMARLEnv):
env = multi_agent_to_single_agent(env)
# save resume path before creating a new log_dir
if agent_cfg.resume or agent_cfg.algorithm.class_name == "Distillation":
resume_path = resolve_resume_path(log_root_path, agent_cfg.load_run, agent_cfg.load_checkpoint)
# wrap for video recording
if args_cli.video:
video_kwargs = {
"video_folder": os.path.join(log_dir, "videos", "train"),
"step_trigger": lambda step: step % args_cli.video_interval == 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)
# create runner from rsl-rl
runner = OnPolicyRunner(env, agent_cfg.to_dict(), log_dir=log_dir, device=agent_cfg.device)
# write git state to logs
runner.add_git_repo_to_log(__file__)
# load the checkpoint
if agent_cfg.resume or agent_cfg.algorithm.class_name == "Distillation":
print(f"[INFO]: Loading model checkpoint from: {resume_path}")
# load previously trained model
runner.load(resume_path)
# dump the configuration into log-directory
dump_yaml(os.path.join(log_dir, "params", "env.yaml"), env_cfg)
dump_yaml(os.path.join(log_dir, "params", "agent.yaml"), agent_cfg)
# run training
runner.learn(num_learning_iterations=agent_cfg.max_iterations, init_at_random_ep_len=True)
# close the simulator
env.close()
if __name__ == "__main__":
# run the main function
main()
# close sim app
simulation_app.close()
@@ -0,0 +1,306 @@
#!/usr/bin/env python3
"""Generate rigid-link STL meshes for the Solenopsis (fire-ant) URDF.
The SolidWorks STEP export uses millimetres and this script intentionally keeps
the STL coordinates in millimetres. The URDF applies a 0.001 mesh scale.
Rigid grouping used here:
* base: root-level fixed parts plus each P65 fixed part and motor plate;
* knee: arm, P65 rotating part, W190 harness and module fasteners;
* wheel: W190 motor/wheel appearance assembly.
The grouping is explicit so it can be audited when the CAD assembly changes.
"""
from __future__ import annotations
import argparse
import json
import math
from pathlib import Path
from OCP.BRep import BRep_Builder
from OCP.BRepBuilderAPI import BRepBuilderAPI_Transform
from OCP.BRepMesh import BRepMesh_IncrementalMesh
from OCP.STEPCAFControl import STEPCAFControl_Reader
from OCP.StlAPI import StlAPI_Writer
from OCP.TCollection import TCollection_ExtendedString
from OCP.TDataStd import TDataStd_Name
from OCP.TDF import TDF_Label
from OCP.TDocStd import TDocStd_Document
from OCP.TopoDS import TopoDS_Compound
from OCP.XCAFApp import XCAFApp_Application
from OCP.XCAFDoc import XCAFDoc_DocumentTool, XCAFDoc_ShapeTool
from OCP.collections import Sequence_TDF_Label
from OCP.gp import gp_Trsf
# SolidWorks root axes -> ROS/URDF axes: (x, y, z)_urdf = (z, x, y)_sw.
SW_TO_URDF = (
(0.0, 0.0, 1.0, 0.0),
(1.0, 0.0, 0.0, 0.0),
(0.0, 1.0, 0.0, 0.0),
(0.0, 0.0, 0.0, 1.0),
)
IDENTITY = (
(1.0, 0.0, 0.0, 0.0),
(0.0, 1.0, 0.0, 0.0),
(0.0, 0.0, 1.0, 0.0),
(0.0, 0.0, 0.0, 1.0),
)
# Standardised URDF moving-link frames. In both cases the arm points along
# link -Z at q=0 and the URDF joint axis is link -Y, matching the established
# controller convention. The right-hand transform mirrors the CAD module in
# the physically correct way while remaining a proper rotation (determinant 1).
MODULE_TO_LEFT_LINK = (
(0.0, 0.0, -1.0, 0.0),
(-1.0, 0.0, 0.0, 0.0),
(0.0, 1.0, 0.0, 0.0),
(0.0, 0.0, 0.0, 1.0),
)
MODULE_TO_RIGHT_LINK = (
(0.0, 0.0, 1.0, 0.0),
(1.0, 0.0, 0.0, 0.0),
(0.0, 1.0, 0.0, 0.0),
(0.0, 0.0, 0.0, 1.0),
)
# The two named components are fixed to the chassis side of the P65 joint.
BASE_COMPONENT_MARKERS = (
"Solenopsis-02-002 关节电机固定板",
"P65MAX-S外观模型-A0-20260205-固定组件",
)
WHEEL_COMPONENT_MARKER = "W190-S 电机外观模型"
def matmul(a, b):
return tuple(
tuple(sum(a[i][k] * b[k][j] for k in range(4)) for j in range(4))
for i in range(4)
)
def transform_point(t, p):
q = (*p, 1.0)
return tuple(sum(t[i][k] * q[k] for k in range(4)) for i in range(3))
def translation(x, y, z):
return (
(1.0, 0.0, 0.0, x),
(0.0, 1.0, 0.0, y),
(0.0, 0.0, 1.0, z),
(0.0, 0.0, 0.0, 1.0),
)
def location_matrix(label):
trsf = XCAFDoc_ShapeTool.GetLocation_s(label).Transformation()
return tuple(
tuple(trsf.Value(i, j) for j in range(1, 5))
if i < 4
else (0.0, 0.0, 0.0, 1.0)
for i in range(1, 5)
)
def gp_transform(matrix):
t = gp_Trsf()
t.SetValues(*(matrix[i][j] for i in range(3) for j in range(4)))
return t
class CadDocument:
def __init__(self, step_path: Path):
self.document = TDocStd_Document(TCollection_ExtendedString("solenopsis"))
application = XCAFApp_Application.GetApplication_s()
application.NewDocument(TCollection_ExtendedString("MDTV-XCAF"), self.document)
reader = STEPCAFControl_Reader()
reader.SetNameMode(True)
reader.SetColorMode(True)
reader.SetPropsMode(True)
status = reader.ReadFile(str(step_path))
if "RetDone" not in str(status):
raise RuntimeError(f"STEP read failed: {status}")
if not reader.Transfer(self.document):
raise RuntimeError("STEP transfer into XCAF failed")
self.shape_tool = XCAFDoc_DocumentTool.ShapeTool_s(self.document.Main())
free = Sequence_TDF_Label()
self.shape_tool.GetFreeShapes(free)
if free.Length() != 1:
raise RuntimeError(f"Expected one root assembly, found {free.Length()}")
self.root = free.Value(1)
@staticmethod
def label_name(label):
attribute = TDataStd_Name()
if label.FindAttribute(TDataStd_Name.GetID_s(), attribute):
return attribute.Get().ToExtString()
return ""
@staticmethod
def referred(label):
target = TDF_Label()
if XCAFDoc_ShapeTool.GetReferredShape_s(label, target):
return target
return label
def referred_name(self, label):
return self.label_name(self.referred(label))
def components(self, label):
target = self.referred(label)
result = Sequence_TDF_Label()
XCAFDoc_ShapeTool.GetComponents_s(target, result, False)
return [result.Value(i) for i in range(1, result.Length() + 1)]
def referred_shape(self, component):
return XCAFDoc_ShapeTool.GetShape_s(self.referred(component))
class ShapeGroup:
def __init__(self):
self.builder = BRep_Builder()
self.compound = TopoDS_Compound()
self.builder.MakeCompound(self.compound)
self.members = []
def add(self, cad: CadDocument, component, output_from_parent, member_name=None):
output_from_component = matmul(output_from_parent, location_matrix(component))
shape = cad.referred_shape(component)
moved = BRepBuilderAPI_Transform(
shape, gp_transform(output_from_component), True, False
).Shape()
self.builder.Add(self.compound, moved)
self.members.append(member_name or cad.referred_name(component))
def write_stl(self, output_path: Path, linear_deflection_mm: float):
output_path.parent.mkdir(parents=True, exist_ok=True)
BRepMesh_IncrementalMesh(
self.compound, linear_deflection_mm, False, 0.35, True
).Perform()
writer = StlAPI_Writer()
# Binary STL is lossless relative to the tessellation and is typically
# five to eight times smaller than the default ASCII representation.
writer.ASCIIMode = False
if not writer.Write(self.compound, str(output_path)):
raise RuntimeError(f"Failed to write {output_path}")
def module_name_from_hip(hip_sw):
# URDF forward = SW +Z, URDF left = SW +X.
fore_aft = "f" if hip_sw[2] > 0.0 else "h"
left_right = "l" if hip_sw[0] > 0.0 else "r"
return fore_aft + left_right
def rotation_rpy(matrix):
"""Return fixed-axis URDF roll/pitch/yaw for a proper rotation matrix."""
pitch = math.asin(max(-1.0, min(1.0, -matrix[2][0])))
if abs(math.cos(pitch)) > 1.0e-10:
roll = math.atan2(matrix[2][1], matrix[2][2])
yaw = math.atan2(matrix[1][0], matrix[0][0])
else:
roll = math.atan2(-matrix[0][1], matrix[1][1])
yaw = 0.0
return roll, pitch, yaw
def generate(step_path: Path, output_dir: Path, deflection_mm: float):
cad = CadDocument(step_path)
base = ShapeGroup()
knees = {}
wheels = {}
manifest = {
"source_step": str(step_path),
"stl_units": "millimetre",
"linear_deflection_mm": deflection_mm,
"axis_map": "URDF (x,y,z) = SolidWorks (z,x,y)",
"modules": {},
}
module_components = []
for component in cad.components(cad.root):
name = cad.referred_name(component)
if name == "Solenopsis-02 运动模组总成":
module_components.append(component)
else:
base.add(cad, component, SW_TO_URDF, name)
if len(module_components) != 4:
raise RuntimeError(f"Expected four motion modules, found {len(module_components)}")
for module in module_components:
root_from_module = location_matrix(module)
hip_sw = transform_point(root_from_module, (0.0, 300.0, 0.0))
leg = module_name_from_hip(hip_sw)
if leg in knees:
raise RuntimeError(f"Duplicate leg classification: {leg}")
knees[leg] = ShapeGroup()
wheels[leg] = ShapeGroup()
# The STEP assembly stores the four legs at four arbitrary joint
# angles. A URDF needs a common neutral pose, so q=0 is defined with
# every 300 mm arm vertical and all established joint axes at -Y.
module_to_link = (
MODULE_TO_LEFT_LINK if leg.endswith("l") else MODULE_TO_RIGHT_LINK
)
knee_from_module = matmul(
module_to_link, translation(0.0, -300.0, 0.0)
)
wheel_from_module = module_to_link
for child in cad.components(module):
name = cad.referred_name(child)
if any(marker in name for marker in BASE_COMPONENT_MARKERS):
base.add(cad, child, matmul(SW_TO_URDF, root_from_module), f"{leg}:{name}")
elif WHEEL_COMPONENT_MARKER in name:
wheels[leg].add(cad, child, wheel_from_module, name)
else:
knees[leg].add(cad, child, knee_from_module, name)
urdf_from_module = matmul(SW_TO_URDF, root_from_module)
hip_urdf_mm = transform_point(SW_TO_URDF, hip_sw)
manifest["modules"][leg] = {
"hip_urdf_m": [value / 1000.0 for value in hip_urdf_mm],
"joint_rpy_rad": [0.0, 0.0, 0.0],
"joint_axis": [0.0, -1.0, 0.0],
"cad_assembly_rpy_rad": list(rotation_rpy(urdf_from_module)),
"base_members": [
name for name in base.members if name.startswith(f"{leg}:")
],
"knee_members": knees[leg].members,
"wheel_members": wheels[leg].members,
}
base.write_stl(output_dir / "base_link.STL", deflection_mm)
for leg in ("fl", "fr", "hl", "hr"):
knees[leg].write_stl(output_dir / f"{leg}_knee.STL", deflection_mm)
wheels[leg].write_stl(output_dir / f"{leg}_wheel.STL", deflection_mm)
(output_dir / "mesh_manifest.json").write_text(
json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
)
def main():
parser = argparse.ArgumentParser()
parser.add_argument("step", type=Path)
parser.add_argument("output_dir", type=Path)
parser.add_argument(
"--deflection-mm",
type=float,
default=0.2,
help="Maximum linear tessellation deflection in millimetres (default: 0.2)",
)
args = parser.parse_args()
generate(args.step.resolve(), args.output_dir.resolve(), args.deflection_mm)
if __name__ == "__main__":
main()
+80
View File
@@ -0,0 +1,80 @@
# Copyright (c) 2025 Deep Robotics
# SPDX-License-Identifier: BSD 3-Clause
# Copyright (c) 2024-2025 Ziqi Fan
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) 2024-2025, The Isaac Lab Project Developers.
# All rights reserved.
#
# SPDX-License-Identifier: Apache-2.0
"""
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 `isaaclab_tasks` extension. They start
with `Isaac` in their name.
"""
"""Launch Isaac Sim Simulator first."""
from isaaclab.app import AppLauncher
# launch omniverse app
app_launcher = AppLauncher(headless=True)
simulation_app = app_launcher.app
"""Rest everything follows."""
import gymnasium as gym
import textwrap
from prettytable import PrettyTable
import rl_training.tasks # noqa: F401
def main():
"""Print all environments registered in `isaaclab_tasks` extension."""
# print all the available environments
table = PrettyTable(["S. No.", "Task Name", "Entry Point", "Config"])
table.title = "Available Environments in Isaac Lab"
# set alignment of table columns
table.align["Task Name"] = "l"
table.align["Entry Point"] = "l"
table.align["Config"] = "l"
table.hrules = 1
# set max width for text wrapping
max_width = 50
# count of environments
index = 0
# acquire all Isaac environments names
for task_spec in gym.registry.values():
if "Deeprobotics" in task_spec.id:
# wrap long text in each column before adding it to the table
task_name = textwrap.fill(task_spec.id, max_width)
entry_point = textwrap.fill(task_spec.entry_point, max_width)
config = textwrap.fill(task_spec.kwargs["env_cfg_entry_point"], max_width)
# add details to table
table.add_row([index + 1, task_name, entry_point, config])
# increment count
index += 1
print(table)
if __name__ == "__main__":
try:
# run the main function
main()
except Exception as e:
raise e
finally:
# close the app
simulation_app.close()
+100
View File
@@ -0,0 +1,100 @@
#!/usr/bin/env python3
"""Create lightweight visual STL meshes by deterministic vertex clustering.
The source meshes are left untouched. This optimization is intended only for
URDF ``visual`` geometry; collision geometry and all physical properties stay
in the URDF. Vertices are snapped to a regular grid, so the maximum movement
of any source vertex is bounded by ``sqrt(3) * grid_mm / 2``.
"""
from __future__ import annotations
import argparse
import json
import math
from pathlib import Path
import numpy as np
import trimesh
def optimize_mesh(source: Path, destination: Path, grid_mm: float) -> dict[str, object]:
mesh = trimesh.load_mesh(source, process=True)
source_vertices = len(mesh.vertices)
source_faces = len(mesh.faces)
# Round each vertex to its nearest grid point. The rounding makes the
# geometric error bounded and gives deterministic output across runs.
clustered_vertices = np.rint(mesh.vertices / grid_mm).astype(np.int64)
unique_grid_vertices, inverse = np.unique(clustered_vertices, axis=0, return_inverse=True)
faces = inverse[mesh.faces]
# Remove triangles collapsed by clustering.
valid = (
(faces[:, 0] != faces[:, 1])
& (faces[:, 1] != faces[:, 2])
& (faces[:, 2] != faces[:, 0])
)
faces = faces[valid]
# Remove duplicate triangles while preserving the winding of the first
# occurrence. Sorting is used only to detect duplicates.
canonical_faces = np.sort(faces, axis=1)
_, first_indices = np.unique(canonical_faces, axis=0, return_index=True)
faces = faces[np.sort(first_indices)]
vertices = unique_grid_vertices.astype(np.float64) * grid_mm
result = trimesh.Trimesh(vertices=vertices, faces=faces, process=False)
result.remove_unreferenced_vertices()
destination.parent.mkdir(parents=True, exist_ok=True)
result.export(destination, file_type="stl")
return {
"file": source.name,
"source_vertices": source_vertices,
"source_faces": source_faces,
"optimized_vertices": len(result.vertices),
"optimized_faces": len(result.faces),
"source_bytes": source.stat().st_size,
"optimized_bytes": destination.stat().st_size,
}
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("source_dir", type=Path)
parser.add_argument("output_dir", type=Path)
parser.add_argument("--grid-mm", type=float, default=1.0)
args = parser.parse_args()
if args.grid_mm <= 0.0:
raise ValueError("--grid-mm must be positive")
source_dir = args.source_dir.resolve()
output_dir = args.output_dir.resolve()
records = [
optimize_mesh(source, output_dir / source.name, args.grid_mm)
for source in sorted(source_dir.glob("*.STL"))
]
if not records:
raise RuntimeError(f"No STL files found in {source_dir}")
manifest = {
"source_dir": str(source_dir),
"grid_mm": args.grid_mm,
"maximum_vertex_displacement_mm": math.sqrt(3.0) * args.grid_mm / 2.0,
"source_faces": sum(int(record["source_faces"]) for record in records),
"optimized_faces": sum(int(record["optimized_faces"]) for record in records),
"source_bytes": sum(int(record["source_bytes"]) for record in records),
"optimized_bytes": sum(int(record["optimized_bytes"]) for record in records),
"meshes": records,
}
(output_dir / "optimization_manifest.json").write_text(
json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
)
print(json.dumps(manifest, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()
+35
View File
@@ -0,0 +1,35 @@
[package]
# Semantic Versioning is used: https://semver.org/
version = "1.0.0"
# Description
category = "isaaclab"
readme = "README.md"
title = "RL Training Repo for DeepRobotics"
author = "Bo Peng"
maintainer = "Bo Peng"
description="RL Training Repo for DeepRobotic, Based on IsaacLab."
repository = "https://github.com/DeepRoboticsLab/rl_training.git"
keywords = ["extension", "rl_training", "isaaclab"]
[dependencies]
"isaaclab" = {}
"isaaclab_assets" = {}
"isaaclab_mimic" = {}
"isaaclab_rl" = {}
"isaaclab_tasks" = {}
# NOTE: Add additional dependencies here
[[python.module]]
name = "rl_training"
[isaaclab_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"
+3
View File
@@ -0,0 +1,3 @@
[build-system]
requires = ["setuptools", "wheel", "toml"]
build-backend = "setuptools.build_meta"
@@ -0,0 +1,15 @@
# Copyright (c) 2025 Deep Robotics
# SPDX-License-Identifier: BSD 3-Clause
#
# # Copyright (c) 2024-2025 Ziqi Fan
# SPDX-License-Identifier: Apache-2.0
"""
Python module serving as a project/extension template.
"""
# Register Gym environments.
from .tasks import *
# Register UI extensions.
from .ui_extension_example import *
@@ -0,0 +1,32 @@
# Copyright (c) 2025 Deep Robotics
# SPDX-License-Identifier: BSD 3-Clause
#
# # Copyright (c) 2024-2025 Ziqi Fan
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) 2022-2025, The Isaac Lab Project Developers.
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
"""Package containing asset and sensor configurations."""
import os
import toml
##
# Configuration for different assets.
##
# Conveniences to other module directories via relative paths
ISAACLAB_ASSETS_EXT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../"))
"""Path to the extension source directory."""
ISAACLAB_ASSETS_DATA_DIR = os.path.join(ISAACLAB_ASSETS_EXT_DIR, "../../deep_robotics_model")
"""Path to the extension data directory."""
ISAACLAB_ASSETS_METADATA = toml.load(os.path.join(ISAACLAB_ASSETS_EXT_DIR, "config", "extension.toml"))
"""Extension metadata dictionary parsed from the extension.toml file."""
# Configure the module-level variables
__version__ = ISAACLAB_ASSETS_METADATA["package"]["version"]
@@ -0,0 +1,124 @@
# Copyright (c) 2025 Deep Robotics
# SPDX-License-Identifier: BSD 3-Clause
# Copyright (c) 2024-2025 Ziqi Fan
# SPDX-License-Identifier: Apache-2.0
import isaaclab.sim as sim_utils
from isaaclab.actuators import DCMotorCfg, DelayedPDActuatorCfg
from isaaclab.assets.articulation import ArticulationCfg
from rl_training.assets import ISAACLAB_ASSETS_DATA_DIR
DEEPROBOTICS_LITE3_CFG = ArticulationCfg(
spawn=sim_utils.UsdFileCfg(
usd_path=f"{ISAACLAB_ASSETS_DATA_DIR}/Lite3/Lite3_usd/Lite3.usd",
activate_contact_sensors=True,
rigid_props=sim_utils.RigidBodyPropertiesCfg(
disable_gravity=False,
retain_accelerations=False,
linear_damping=0.0,
angular_damping=0.0,
max_linear_velocity=1000.0,
max_angular_velocity=1000.0,
max_depenetration_velocity=1.0,
),
articulation_props=sim_utils.ArticulationRootPropertiesCfg(
enabled_self_collisions=False, solver_position_iteration_count=4, solver_velocity_iteration_count=1
),
),
init_state=ArticulationCfg.InitialStateCfg(
pos=(0.0, 0.0, 0.35),
joint_pos={
".*Knee_joint": 0.0,
},
joint_vel={".*": 0.0},
),
soft_joint_pos_limit_factor=0.99,
actuators={
# "Hip": DelayedPDActuatorCfg(
# joint_names_expr=[".*_Hip[X,Y]_joint"],
# effort_limit=24.0,
# velocity_limit=26.2,
# stiffness=30.0,
# damping=1.0,
# friction=0.0,
# armature=0.0,
# min_delay=0,
# max_delay=5,
# ),
"Knee": DelayedPDActuatorCfg(
joint_names_expr=[".*_Knee_joint"],
effort_limit=36.0,
velocity_limit=26.2,
stiffness=30.0,
damping=1.0,
friction=0.0,
armature=0.0,
min_delay=0,
max_delay=5,
),
},
)
DEEPROBOTICS_M20_CFG = ArticulationCfg(
spawn=sim_utils.UrdfFileCfg(
asset_path=f"{ISAACLAB_ASSETS_DATA_DIR}/M20/M20_urdf/urdf/Solenopsis.urdf",
fix_base=False,
merge_fixed_joints=True,
joint_drive=None,
activate_contact_sensors=True,
visual_material=sim_utils.PreviewSurfaceCfg(
diffuse_color=(0.42, 0.45, 0.50),
roughness=0.65,
metallic=0.15,
),
rigid_props=sim_utils.RigidBodyPropertiesCfg(
disable_gravity=False,
retain_accelerations=False,
linear_damping=0.0,
angular_damping=0.0,
max_linear_velocity=1000.0,
max_angular_velocity=1000.0,
max_depenetration_velocity=1.0,
),
articulation_props=sim_utils.ArticulationRootPropertiesCfg(
enabled_self_collisions=False, solver_position_iteration_count=4, solver_velocity_iteration_count=1
),
),
init_state=ArticulationCfg.InitialStateCfg(
pos=(0.0, 0.0, 0.306),
joint_pos={
"f[l,r]_knee_joint": 0.5,
"h[l,r]_knee_joint": -0.5,
".*wheel_joint": 0.0,
},
joint_vel={".*": 0.0},
),
soft_joint_pos_limit_factor=0.9,
actuators={
"joint": DelayedPDActuatorCfg(
joint_names_expr=[".*knee_joint"],
effort_limit=75.0,
velocity_limit=17.8024,
stiffness=80.0,
damping=2.0,
friction=0.0,
armature=0.0,
min_delay=0,
max_delay=5,
),
"wheel": DelayedPDActuatorCfg(
joint_names_expr=[".*_wheel_joint"],
effort_limit=28.68,
velocity_limit=109.956,
stiffness=0.0,
damping=0.6,
friction=1.0,
armature=0.00243216,
min_delay=0,
max_delay=5,
),
},
)
@@ -0,0 +1,27 @@
# Copyright (c) 2025 Deep Robotics
# SPDX-License-Identifier: BSD 3-Clause
#
# # Copyright (c) 2024-2025 Ziqi Fan
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) 2022-2025, The Isaac Lab Project Developers.
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
"""Package containing task implementations for various robotic environments."""
import os
import toml
from isaaclab_tasks.utils import import_packages
##
# Register Gym environments.
##
# The blacklist is used to prevent importing configs from sub-packages
_BLACKLIST_PKGS = ["utils"]
# Import all configs in this package
import_packages(__name__, _BLACKLIST_PKGS)
@@ -0,0 +1,16 @@
# Copyright (c) 2025 Deep Robotics
# SPDX-License-Identifier: BSD 3-Clause
#
# # Copyright (c) 2024-2025 Ziqi Fan
# SPDX-License-Identifier: Apache-2.0
# 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
"""
Config-based workflow environments.
"""
import gymnasium as gym
@@ -0,0 +1,14 @@
# Copyright (c) 2025 Deep Robotics
# SPDX-License-Identifier: BSD 3-Clause
#
# # Copyright (c) 2024-2025 Ziqi Fan
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) 2022-2025, The Isaac Lab Project Developers.
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
"""Locomotion environments for legged robots."""
from .velocity import * # noqa
@@ -0,0 +1,18 @@
# Copyright (c) 2025 Deep Robotics
# SPDX-License-Identifier: BSD 3-Clause
#
# # Copyright (c) 2024-2025 Ziqi Fan
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) 2022-2025, The Isaac Lab Project Developers.
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
"""Locomotion environments with velocity-tracking commands.
These environments are based on the `legged_gym` environments provided by Rudin et al.
Reference:
https://github.com/leggedrobotics/legged_gym
"""
@@ -0,0 +1,15 @@
# Copyright (c) 2025 Deep Robotics
# SPDX-License-Identifier: BSD 3-Clause
#
# # Copyright (c) 2024-2025 Ziqi Fan
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) 2022-2025, The Isaac Lab Project Developers.
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
"""Configurations for velocity-based locomotion environments."""
# We leave this file empty since we don't want to expose any configs in this package directly.
# We still need this file to import the "config" module in the parent package.
@@ -0,0 +1,15 @@
# Copyright (c) 2025 Deep Robotics
# SPDX-License-Identifier: BSD 3-Clause
#
# # Copyright (c) 2024-2025 Ziqi Fan
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) 2022-2025, The Isaac Lab Project Developers.
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
"""Configurations for velocity-based locomotion environments."""
# We leave this file empty since we don't want to expose any configs in this package directly.
# We still need this file to import the "config" module in the parent package.
@@ -0,0 +1,36 @@
# Copyright (c) 2025 Deep Robotics
# SPDX-License-Identifier: BSD 3-Clause
# Copyright (c) 2024-2025 Ziqi Fan
# SPDX-License-Identifier: Apache-2.0
import gymnasium as gym
from . import agents
##
# Register Gym environments.
##
gym.register(
id="Flat-Deeprobotics-Lite3-v0",
entry_point="isaaclab.envs:ManagerBasedRLEnv",
disable_env_checker=True,
kwargs={
"env_cfg_entry_point": f"{__name__}.flat_env_cfg:DeeproboticsLite3FlatEnvCfg",
"rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:DeeproboticsLite3FlatPPORunnerCfg",
"cusrl_cfg_entry_point": f"{agents.__name__}.cusrl_ppo_cfg:DeeproboticsLite3FlatTrainerCfg",
},
)
gym.register(
id="Rough-Deeprobotics-Lite3-v0",
entry_point="isaaclab.envs:ManagerBasedRLEnv",
disable_env_checker=True,
kwargs={
"env_cfg_entry_point": f"{__name__}.rough_env_cfg:DeeproboticsLite3RoughEnvCfg",
"rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:DeeproboticsLite3RoughPPORunnerCfg",
"cusrl_cfg_entry_point": f"{agents.__name__}.cusrl_ppo_cfg:DeeproboticsLite3RoughTrainerCfg",
},
)
@@ -0,0 +1,5 @@
# Copyright (c) 2025 Deep Robotics
# SPDX-License-Identifier: BSD 3-Clause
#
# # Copyright (c) 2024-2025 Ziqi Fan
# SPDX-License-Identifier: Apache-2.0
@@ -0,0 +1,48 @@
# Copyright (c) 2025 Deep Robotics
# SPDX-License-Identifier: BSD 3-Clause
# Copyright (c) 2024-2025 Ziqi Fan
# SPDX-License-Identifier: Apache-2.0
from isaaclab.utils import configclass
from isaaclab_rl.rsl_rl import RslRlOnPolicyRunnerCfg, RslRlPpoActorCriticCfg, RslRlPpoAlgorithmCfg
@configclass
class DeeproboticsLite3RoughPPORunnerCfg(RslRlOnPolicyRunnerCfg):
num_steps_per_env = 24
max_iterations = 10000
save_interval = 100
experiment_name = "deeprobotics_lite3_rough"
empirical_normalization = False
clip_actions = 100
policy = RslRlPpoActorCriticCfg(
init_noise_std=1.0,
noise_std_type="log",
actor_hidden_dims=[512, 256, 128],
critic_hidden_dims=[512, 256, 128],
activation="elu",
)
algorithm = RslRlPpoAlgorithmCfg(
value_loss_coef=1.0,
use_clipped_value_loss=True,
clip_param=0.2,
entropy_coef=0.01,
num_learning_epochs=5,
num_mini_batches=4,
learning_rate=1.0e-3,
schedule="adaptive",
gamma=0.99,
lam=0.95,
desired_kl=0.01,
max_grad_norm=1.0,
)
@configclass
class DeeproboticsLite3FlatPPORunnerCfg(DeeproboticsLite3RoughPPORunnerCfg):
def __post_init__(self):
super().__post_init__()
self.max_iterations = 10000
self.experiment_name = "deeprobotics_lite3_flat"
@@ -0,0 +1,32 @@
# Copyright (c) 2025 Deep Robotics
# SPDX-License-Identifier: BSD 3-Clause
# Copyright (c) 2024-2025 Ziqi Fan
# SPDX-License-Identifier: Apache-2.0
from isaaclab.utils import configclass
from .rough_env_cfg import DeeproboticsLite3RoughEnvCfg
@configclass
class DeeproboticsLite3FlatEnvCfg(DeeproboticsLite3RoughEnvCfg):
def __post_init__(self):
# post init of parent
super().__post_init__()
# override rewards
# self.rewards.base_height_l2.params["sensor_cfg"] = None
# change terrain to flat
self.scene.terrain.terrain_type = "plane"
self.scene.terrain.terrain_generator = None
# no height scan
# self.scene.height_scanner = None
self.observations.policy.height_scan = None
# self.observations.critic.height_scan = None
# no terrain curriculum
self.curriculum.terrain_levels = None
# If the weight of rewards is 0, set rewards to None
if self.__class__.__name__ == "DeeproboticsLite3FlatEnvCfg":
self.disable_zero_weight_rewards()
@@ -0,0 +1,183 @@
# Copyright (c) 2025 Deep Robotics
# SPDX-License-Identifier: BSD 3-Clause
# Copyright (c) 2024-2025 Ziqi Fan
# SPDX-License-Identifier: Apache-2.0
from isaaclab.utils import configclass
from rl_training.tasks.manager_based.locomotion.velocity.velocity_env_cfg import LocomotionVelocityRoughEnvCfg
# from isaaclab.sensors.ray_caster import GridPatternCfg
##
# Pre-defined configs
##
from rl_training.assets.deeprobotics import DEEPROBOTICS_LITE3_CFG # isort: skip
@configclass
class DeeproboticsLite3RoughEnvCfg(LocomotionVelocityRoughEnvCfg):
base_link_name = "TORSO"
foot_link_name = ".*_FOOT"
# fmt: off
joint_names = [
"FL_HipX_joint", "FL_HipY_joint", "FL_Knee_joint",
"FR_HipX_joint", "FR_HipY_joint", "FR_Knee_joint",
"HL_HipX_joint", "HL_HipY_joint", "HL_Knee_joint",
"HR_HipX_joint", "HR_HipY_joint", "HR_Knee_joint",
]
link_names = [
'TORSO',
'FL_HIP', 'FR_HIP', 'HL_HIP', 'HR_HIP',
'FL_THIGH', 'FR_THIGH', 'HL_THIGH', 'HR_THIGH',
'FL_SHANK', 'FR_SHANK', 'HL_SHANK', 'HR_SHANK',
'FL_FOOT', 'FR_FOOT', 'HL_FOOT', 'HR_FOOT',
]
# fmt: on
def __post_init__(self):
# post init of parent
super().__post_init__()
# ------------------------------Sence------------------------------
self.scene.robot = DEEPROBOTICS_LITE3_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot")
self.scene.height_scanner.prim_path = "{ENV_REGEX_NS}/Robot/" + self.base_link_name
self.scene.height_scanner_base.prim_path = "{ENV_REGEX_NS}/Robot/" + self.base_link_name
self.scene.height_scanner.pattern_cfg.resolution = 0.07 # = GridPatternCfg(resolution=0.07, size=[1.6, 1.0]),
# ------------------------------Observations------------------------------
self.observations.policy.base_lin_vel = None # type: ignore
self.observations.policy.height_scan = None # type: ignore
self.observations.policy.base_ang_vel.scale = 0.25
self.observations.policy.joint_pos.scale = 1.0
self.observations.policy.joint_vel.scale = 0.05
self.observations.policy.joint_pos.params["asset_cfg"].joint_names = self.joint_names
self.observations.policy.joint_vel.params["asset_cfg"].joint_names = self.joint_names
# ------------------------------Actions------------------------------
# reduce action scale
self.actions.joint_pos.scale = {".*_HipX_joint": 0.125, "^(?!.*_HipX_joint).*": 0.25}
self.actions.joint_pos.clip = {".*": (-100.0, 100.0)}
self.actions.joint_pos.joint_names = self.joint_names
# ------------------------------Events------------------------------
self.events.randomize_reset_base.params = {
"pose_range": {
"x": (-1.0, 1.0),
"y": (-1.0, 1.0),
"z": (0.0, 0.0),
"roll": (-0.3, 0.3),
"pitch": (-0.3, 0.3),
"yaw": (-3.14, 3.14),
},
"velocity_range": {
"x": (-0.2, 0.2),
"y": (-0.2, 0.2),
"z": (-0.2, 0.2),
"roll": (-0.05, 0.05),
"pitch": (-0.05, 0.05),
"yaw": (-0.0, 0.0),
},
}
self.events.randomize_rigid_body_mass.params["asset_cfg"].body_names = self.link_names # [self.base_link_name]
self.events.randomize_rigid_body_mass_base = None
self.events.randomize_com_positions.params["asset_cfg"].body_names = self.base_link_name # [self.base_link_name]
# self.events.randomize_com_positions = None
self.events.randomize_apply_external_force_torque = None
self.events.randomize_push_robot = None
self.events.randomize_actuator_gains.params["asset_cfg"].joint_names = self.joint_names
# set terrain generation probability to 0 for boxes and stairs
self.scene.terrain.terrain_generator.sub_terrains["random_rough"].proportion = 0.4
self.scene.terrain.terrain_generator.sub_terrains["hf_pyramid_slope"].proportion = 0.3
self.scene.terrain.terrain_generator.sub_terrains["hf_pyramid_slope_inv"].proportion = 0.3
self.scene.terrain.terrain_generator.sub_terrains["boxes"].proportion = 0.0
self.scene.terrain.terrain_generator.sub_terrains["pyramid_stairs"].proportion = 0.0
self.scene.terrain.terrain_generator.sub_terrains["pyramid_stairs_inv"].proportion = 0.0
# scale down the terrains because the robot is small
# self.scene.terrain.terrain_generator.sub_terrains["boxes"].grid_height_range = (0.025, 0.1)
# self.scene.terrain.terrain_generator.sub_terrains["boxes"].grid_width = 0.8
self.scene.terrain.terrain_generator.sub_terrains["random_rough"].noise_range = (0.01, 0.06)
self.scene.terrain.terrain_generator.sub_terrains["random_rough"].noise_step = 0.01
# ------------------------------Rewards------------------------------
self.rewards.action_rate_l2.weight = -0.02 #-0.02
# self.rewards.smoothness_2.weight = -0.0075
self.rewards.base_height_l2.weight = -10.0
self.rewards.base_height_l2.params["target_height"] = 0.35
self.rewards.base_height_l2.params["asset_cfg"].body_names = [self.base_link_name]
self.rewards.feet_air_time.weight = 5.0 # 5.0
self.rewards.feet_air_time.params["threshold"] = 0.5
self.rewards.feet_air_time.params["sensor_cfg"].body_names = [self.foot_link_name]
self.rewards.feet_air_time_variance.weight = -8.0 # -8.0
self.rewards.feet_air_time_variance.params["sensor_cfg"].body_names = [self.foot_link_name]
self.rewards.feet_slide.weight = -0.05
self.rewards.feet_slide.params["sensor_cfg"].body_names = [self.foot_link_name]
self.rewards.feet_slide.params["asset_cfg"].body_names = [self.foot_link_name]
self.rewards.stand_still.weight = -0.5 # -1.0
self.rewards.stand_still.params["asset_cfg"].joint_names = self.joint_names
self.rewards.stand_still.params["command_threshold"] = 0.1
self.rewards.feet_height_body.weight = -2.5 # -2.5
self.rewards.feet_height_body.params["target_height"] = -0.35
self.rewards.feet_height_body.params["asset_cfg"].body_names = [self.foot_link_name]
self.rewards.feet_height.weight = -0.2 # -0.2
self.rewards.feet_height.params["asset_cfg"].body_names = [self.foot_link_name]
self.rewards.feet_height.params["target_height"] = 0.05
self.rewards.contact_forces.weight = -1e-1 # -2e-2
self.rewards.contact_forces.params["sensor_cfg"].body_names = [self.foot_link_name]
self.rewards.lin_vel_z_l2.weight = -2.0 #-2.0
self.rewards.ang_vel_xy_l2.weight = -0.05 # -0.05
self.rewards.track_lin_vel_xy_exp.weight = 3.0
self.rewards.track_ang_vel_z_exp.weight = 1.5
self.rewards.undesired_contacts.weight = -0.5
self.rewards.undesired_contacts.params["sensor_cfg"].body_names = [f"^(?!.*{self.foot_link_name}).*"]
self.rewards.joint_torques_l2.weight = -2.5e-5
self.rewards.joint_acc_l2.weight = -1e-8
self.rewards.joint_deviation_l1.weight = -0.5
self.rewards.joint_deviation_l1.params["asset_cfg"].joint_names = [".*HipX.*"]
self.rewards.joint_power.weight = -2e-5
self.rewards.flat_orientation_l2.weight = -5.0
# add the following rewards to improve the gait
self.rewards.feet_gait.weight = 0.5
self.rewards.feet_gait.params["synced_feet_pair_names"] = [
["FL_FOOT", "HR_FOOT"],
["FR_FOOT", "HL_FOOT"]
]
self.rewards.joint_mirror.weight = -0.05
self.rewards.joint_mirror.params["mirror_joints"] = [
["FL_(HipX|HipY|Knee).*", "HR_(HipX|HipY|Knee).*"],
["FR_(HipX|HipY|Knee).*", "HL_(HipX|HipY|Knee).*"],
]
self.rewards.joint_pos_limits.weight = -5.0
# self.rewards.joint_pos_penalty.weight = -1.0
self.rewards.feet_contact_without_cmd.weight = 0.1
self.rewards.feet_contact_without_cmd.params["sensor_cfg"].body_names = [self.foot_link_name]
# If the weight of rewards is 0, set rewards to None
if self.__class__.__name__ == "DeeproboticsLite3RoughEnvCfg":
self.disable_zero_weight_rewards()
# ------------------------------Terminations------------------------------
self.terminations.illegal_contact = None
# self.terminations.bad_orientation_2 = None
# ------------------------------Curriculums------------------------------
# self.curriculum.command_levels.params["range_multiplier"] = (0.2, 1.0)
self.curriculum.command_levels = None
# ------------------------------Commands------------------------------
self.commands.base_velocity.ranges.lin_vel_x = (-1.5, 1.5)
self.commands.base_velocity.ranges.lin_vel_y = (-0.8, 0.8)
self.commands.base_velocity.ranges.ang_vel_z = (-0.8, 0.8)
@@ -0,0 +1,15 @@
# Copyright (c) 2025 Deep Robotics
# SPDX-License-Identifier: BSD 3-Clause
#
# # Copyright (c) 2024-2025 Ziqi Fan
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) 2022-2025, The Isaac Lab Project Developers.
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
"""Configurations for velocity-based locomotion environments."""
# We leave this file empty since we don't want to expose any configs in this package directly.
# We still need this file to import the "config" module in the parent package.
@@ -0,0 +1,46 @@
# Copyright (c) 2025 Deep Robotics
# SPDX-License-Identifier: BSD 3-Clause
#
# # Copyright (c) 2024-2025 Ziqi Fan
# SPDX-License-Identifier: Apache-2.0
import gymnasium as gym
from . import agents
##
# Register Gym environments.
##
gym.register(
id="Flat-Deeprobotics-M20-v0",
entry_point="isaaclab.envs:ManagerBasedRLEnv",
disable_env_checker=True,
kwargs={
"env_cfg_entry_point": f"{__name__}.flat_env_cfg:DeeproboticsM20FlatEnvCfg",
"rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:DeeproboticsM20FlatPPORunnerCfg",
"cusrl_cfg_entry_point": f"{agents.__name__}.cusrl_ppo_cfg:DeeproboticsM20FlatTrainerCfg",
},
)
gym.register(
id="Rough-Deeprobotics-M20-v0",
entry_point="isaaclab.envs:ManagerBasedRLEnv",
disable_env_checker=True,
kwargs={
"env_cfg_entry_point": f"{__name__}.rough_env_cfg:DeeproboticsM20RoughEnvCfg",
"rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:DeeproboticsM20RoughPPORunnerCfg",
"cusrl_cfg_entry_point": f"{agents.__name__}.cusrl_ppo_cfg:DeeproboticsM20RoughTrainerCfg",
},
)
gym.register(
id="Rough-Deeprobotics-M20-Support-v0",
entry_point="isaaclab.envs:ManagerBasedRLEnv",
disable_env_checker=True,
kwargs={
"env_cfg_entry_point": f"{__name__}.rough_env_cfg:DeeproboticsM20SupportRoughEnvCfg",
"rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:DeeproboticsM20SupportRoughPPORunnerCfg",
"cusrl_cfg_entry_point": f"{agents.__name__}.cusrl_ppo_cfg:DeeproboticsM20RoughTrainerCfg",
},
)
@@ -0,0 +1,5 @@
# Copyright (c) 2025 Deep Robotics
# SPDX-License-Identifier: BSD 3-Clause
#
# # Copyright (c) 2024-2025 Ziqi Fan
# SPDX-License-Identifier: Apache-2.0
@@ -0,0 +1,56 @@
# Copyright (c) 2025 Deep Robotics
# SPDX-License-Identifier: BSD 3-Clause
# Copyright (c) 2024-2025 Ziqi Fan
# SPDX-License-Identifier: Apache-2.0
from isaaclab.utils import configclass
from isaaclab_rl.rsl_rl import RslRlOnPolicyRunnerCfg, RslRlPpoActorCriticCfg, RslRlPpoAlgorithmCfg
@configclass
class DeeproboticsM20RoughPPORunnerCfg(RslRlOnPolicyRunnerCfg):
num_steps_per_env = 24
max_iterations = 20000
save_interval = 100
experiment_name = "deeprobotics_m20_rough"
empirical_normalization = False
clip_actions = 100
policy = RslRlPpoActorCriticCfg(
init_noise_std=1.0,
noise_std_type="log",
actor_hidden_dims=[512, 256, 128],
critic_hidden_dims=[512, 256, 128],
activation="elu",
)
algorithm = RslRlPpoAlgorithmCfg(
value_loss_coef=1.0,
use_clipped_value_loss=True,
clip_param=0.2,
entropy_coef=0.01,
num_learning_epochs=5,
num_mini_batches=4,
learning_rate=1.0e-3,
schedule="adaptive",
gamma=0.99,
lam=0.95,
desired_kl=0.01,
max_grad_norm=1.0,
)
@configclass
class DeeproboticsM20FlatPPORunnerCfg(DeeproboticsM20RoughPPORunnerCfg):
def __post_init__(self):
super().__post_init__()
self.max_iterations = 5000
self.experiment_name = "deeprobotics_m20_flat"
@configclass
class DeeproboticsM20SupportRoughPPORunnerCfg(DeeproboticsM20RoughPPORunnerCfg):
def __post_init__(self):
super().__post_init__()
self.experiment_name = "deeprobotics_m20_support"
@@ -0,0 +1,32 @@
# Copyright (c) 2025 Deep Robotics
# SPDX-License-Identifier: BSD 3-Clause
# Copyright (c) 2024-2025 Ziqi Fan
# SPDX-License-Identifier: Apache-2.0
from isaaclab.utils import configclass
from .rough_env_cfg import DeeproboticsM20RoughEnvCfg
@configclass
class DeeproboticsM20FlatEnvCfg(DeeproboticsM20RoughEnvCfg):
def __post_init__(self):
# post init of parent
super().__post_init__()
# override rewards
self.rewards.base_height_l2.params["sensor_cfg"] = None
# change terrain to flat
self.scene.terrain.terrain_type = "plane"
self.scene.terrain.terrain_generator = None
# no height scan
self.scene.height_scanner = None
self.observations.policy.height_scan = None
self.observations.critic.height_scan = None
# no terrain curriculum
self.curriculum.terrain_levels = None
# If the weight of rewards is 0, set rewards to None
if self.__class__.__name__ == "DeeproboticsM20FlatEnvCfg":
self.disable_zero_weight_rewards()
@@ -0,0 +1,504 @@
# Copyright (c) 2025 Deep Robotics
# SPDX-License-Identifier: BSD 3-Clause
# Copyright (c) 2024-2025 Ziqi Fan
# SPDX-License-Identifier: Apache-2.0
from isaaclab.managers import CurriculumTermCfg as CurrTerm
from isaaclab.managers import EventTermCfg as EventTerm
from isaaclab.managers import RewardTermCfg as RewTerm
from isaaclab.managers import ObservationTermCfg as ObsTerm
from isaaclab.managers import SceneEntityCfg
from isaaclab.utils import configclass
import rl_training.tasks.manager_based.locomotion.velocity.mdp as mdp
from rl_training.tasks.manager_based.locomotion.velocity.velocity_env_cfg import (
ActionsCfg,
EventCfg,
LocomotionVelocityRoughEnvCfg,
RewardsCfg,
)
##
# Pre-defined configs
##
from rl_training.assets.deeprobotics import DEEPROBOTICS_M20_CFG # isort: skip
@configclass
class DeeproboticsM20ActionsCfg(ActionsCfg):
"""Action specifications for the MDP."""
joint_pos = mdp.JointPositionActionCfg(
asset_name="robot", joint_names=[""], scale=0.25, use_default_offset=True, clip=None, preserve_order=True
)
joint_vel = mdp.JointVelocityActionCfg(
asset_name="robot", joint_names=[""], scale=20.0, use_default_offset=True, clip=None, preserve_order=True
)
@configclass
class DeeproboticsM20EventCfg(EventCfg):
"""Domain randomization with tire-specific contact material."""
# Replace the inherited all-body material randomization with separate tire and support materials.
randomize_rigid_body_material = None
randomize_wheel_material = EventTerm(
func=mdp.randomize_rigid_body_material,
mode="startup",
params={
"asset_cfg": SceneEntityCfg("robot", body_names=".*_wheel"),
"static_friction_range": (0.9, 1.4),
"dynamic_friction_range": (0.6, 0.9),
"restitution_range": (0.0, 0.05),
"num_buckets": 1024,
},
)
randomize_support_material = EventTerm(
func=mdp.randomize_rigid_body_material,
mode="startup",
params={
"asset_cfg": SceneEntityCfg("robot", body_names=["base_link", ".*_knee"]),
"static_friction_range": (0.4, 0.8),
"dynamic_friction_range": (0.25, 0.4),
"restitution_range": (0.0, 0.05),
"num_buckets": 1024,
},
)
@configclass
class DeeproboticsM20RewardsCfg(RewardsCfg):
"""Reward terms for the MDP."""
joint_vel_wheel_l2 = RewTerm(
func=mdp.joint_vel_l2, weight=0.0, params={"asset_cfg": SceneEntityCfg("robot", joint_names="")}
)
joint_acc_wheel_l2 = RewTerm(
func=mdp.joint_acc_l2, weight=0.0, params={"asset_cfg": SceneEntityCfg("robot", joint_names="")}
)
joint_torques_wheel_l2 = RewTerm(
func=mdp.joint_torques_l2, weight=0.0, params={"asset_cfg": SceneEntityCfg("robot", joint_names="")}
)
track_diff_wheel_yaw_exp = RewTerm(
func=mdp.track_diff_wheel_yaw_exp,
weight=1.0,
params={
"command_name": "base_velocity",
"std": 0.6,
"wheel_radius": 0.096,
"track_width": 0.50,
"left_wheel_indices": (0, 2),
"right_wheel_indices": (1, 3),
"command_threshold": 0.05,
"asset_cfg": SceneEntityCfg("robot", joint_names=""),
},
)
lin_vel_xy_yaw_command_l2 = RewTerm(
func=mdp.lin_vel_xy_yaw_command_l2,
weight=-0.4,
params={
"command_name": "base_velocity",
"yaw_command_threshold": 0.25,
"lin_command_threshold": 0.15,
"asset_cfg": SceneEntityCfg("robot"),
},
)
command_progress = RewTerm(
func=mdp.command_progress,
weight=0.0,
params={
"command_name": "base_velocity",
"min_command": 0.12,
"max_reward_speed": 0.8,
"asset_cfg": SceneEntityCfg("robot"),
},
)
support_contact_progress = RewTerm(
func=mdp.ObstacleSupportEscapeReward,
weight=0.0,
params={
"command_name": "base_velocity",
"sensor_cfg": SceneEntityCfg("contact_forces", body_names=""),
"wheel_sensor_cfg": SceneEntityCfg("contact_forces", body_names=""),
"asset_cfg": SceneEntityCfg("robot"),
"wheel_asset_cfg": SceneEntityCfg("robot", joint_names=""),
"wheel_radius": 0.096,
"support_force_threshold": 15.0,
"wheel_obstacle_force_threshold": 35.0,
"max_contact_time": 0.45,
"min_command": 0.15,
"stall_speed_ratio": 0.35,
"min_stall_speed": 0.12,
"slip_threshold": 0.30,
"obstacle_memory_time": 0.60,
"support_window_time": 0.80,
"recovery_speed_ratio": 0.60,
"min_recovery_speed": 0.18,
"recovery_reward_time": 0.25,
"cooldown_time": 0.60,
"recovery_bonus": 0.25,
"max_reward_speed": 0.8,
},
)
support_contact_excess = RewTerm(
func=mdp.support_contact_excess,
weight=0.0,
params={
"sensor_cfg": SceneEntityCfg("contact_forces", body_names=""),
"force_threshold": 120.0,
"max_contact_time": 0.6,
},
)
@configclass
class DeeproboticsM20RoughEnvCfg(LocomotionVelocityRoughEnvCfg):
"""Stage 1: blind tactile locomotion without body-support rewards."""
actions: DeeproboticsM20ActionsCfg = DeeproboticsM20ActionsCfg()
events: DeeproboticsM20EventCfg = DeeproboticsM20EventCfg()
rewards: DeeproboticsM20RewardsCfg = DeeproboticsM20RewardsCfg()
base_link_name = "base_link"
foot_link_name = ".*_wheel"
support_link_names = ["base_link", ".*_knee"]
# fmt: off
leg_joint_names = [
"fl_knee_joint",
"fr_knee_joint",
"hl_knee_joint",
"hr_knee_joint",
]
wheel_joint_names = [
"fl_wheel_joint", "fr_wheel_joint", "hl_wheel_joint", "hr_wheel_joint",
]
knee_joint_names = [
"fl_knee_joint", "fr_knee_joint", "hl_knee_joint", "hr_knee_joint",
]
joint_names = leg_joint_names + wheel_joint_names
# fmt: on
def __post_init__(self):
# post init of parent
super().__post_init__()
# ------------------------------Sence------------------------------
self.scene.robot = DEEPROBOTICS_M20_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot")
self.scene.height_scanner.prim_path = "{ENV_REGEX_NS}/Robot/" + self.base_link_name
self.scene.height_scanner_base.prim_path = "{ENV_REGEX_NS}/Robot/" + self.base_link_name
self.scene.terrain.physics_material.restitution = 0.0
self.sim.physics_material = self.scene.terrain.physics_material
# ------------------------------Observations------------------------------
self.observations.policy.joint_pos.func = mdp.joint_pos_rel_without_wheel
self.observations.policy.joint_pos.params["wheel_asset_cfg"] = SceneEntityCfg(
"robot", joint_names=self.wheel_joint_names
)
self.observations.critic.joint_pos.func = mdp.joint_pos_rel_without_wheel
self.observations.critic.joint_pos.params["wheel_asset_cfg"] = SceneEntityCfg(
"robot", joint_names=self.wheel_joint_names
)
self.observations.policy.base_lin_vel.scale = 2.0
self.observations.policy.base_ang_vel.scale = 0.25
self.observations.policy.joint_pos.scale = 1.0
self.observations.policy.joint_vel.scale = 0.05
self.observations.policy.base_lin_vel = None
self.observations.policy.height_scan = None
self.observations.policy.joint_pos.params["asset_cfg"].joint_names = self.joint_names
self.observations.policy.joint_vel.params["asset_cfg"].joint_names = self.joint_names
self.observations.policy.wheel_contact = ObsTerm(
func=mdp.contact_state,
params={
"sensor_cfg": SceneEntityCfg("contact_forces", body_names=[self.foot_link_name]),
"threshold": 2.0,
},
clip=(0.0, 1.0),
scale=1.0,
)
self.observations.policy.support_contact = ObsTerm(
func=mdp.contact_state,
params={
"sensor_cfg": SceneEntityCfg("contact_forces", body_names=self.support_link_names),
"threshold": 2.0,
},
clip=(0.0, 1.0),
scale=1.0,
)
self.observations.policy.wheel_contact_force = ObsTerm(
func=mdp.contact_force_norm,
params={
"sensor_cfg": SceneEntityCfg("contact_forces", body_names=[self.foot_link_name]),
"scale": 100.0,
"clip": 2.0,
},
clip=(0.0, 2.0),
scale=1.0,
)
self.observations.policy.support_contact_force = ObsTerm(
func=mdp.contact_force_norm,
params={
"sensor_cfg": SceneEntityCfg("contact_forces", body_names=self.support_link_names),
"scale": 100.0,
"clip": 2.0,
},
clip=(0.0, 2.0),
scale=1.0,
)
self.observations.policy.wheel_slip = ObsTerm(
func=mdp.wheel_slip_proxy,
params={
"asset_cfg": SceneEntityCfg("robot", joint_names=self.wheel_joint_names, preserve_order=True),
"wheel_radius": 0.096,
},
clip=(0.0, 3.0),
scale=0.5,
)
self.observations.policy.applied_torque = ObsTerm(
func=mdp.applied_torque,
params={
"asset_cfg": SceneEntityCfg("robot", joint_names=self.joint_names, preserve_order=True),
"scale": 80.0,
"clip": 2.0,
},
clip=(-2.0, 2.0),
scale=1.0,
)
# ------------------------------Actions------------------------------
# reduce action scale
self.actions.joint_pos.scale = 0.25
self.actions.joint_vel.scale = 5.0
self.actions.joint_pos.clip = {".*": (-100.0, 100.0)}
self.actions.joint_vel.clip = {".*": (-100.0, 100.0)}
self.actions.joint_pos.joint_names = self.leg_joint_names
self.actions.joint_vel.joint_names = self.wheel_joint_names
# ------------------------------Events------------------------------
self.events.randomize_reset_base.params = {
"pose_range": {
"x": (-1.0, 1.0),
"y": (-1.0, 1.0),
"z": (0.0, 0.0),
"roll": (-0.3, 0.3),
"pitch": (-0.3, 0.3),
"yaw": (-3.14, 3.14),
},
"velocity_range": {
"x": (-0.2, 0.2),
"y": (-0.2, 0.2),
"z": (-0.2, 0.2),
"roll": (-0.05, 0.05),
"pitch": (-0.05, 0.05),
"yaw": (-0.0, 0.0),
},
}
self.events.randomize_rigid_body_mass_base.params["asset_cfg"].body_names = [self.base_link_name]
self.events.randomize_rigid_body_mass.params["asset_cfg"].body_names = [
f"^(?!.*{self.base_link_name}).*"
]
self.events.randomize_com_positions.params["asset_cfg"].body_names = [self.base_link_name]
self.events.randomize_apply_external_force_torque.params["asset_cfg"].body_names = [self.base_link_name]
self.scene.terrain.max_init_terrain_level = 2
self.scene.terrain.terrain_generator.sub_terrains["boxes"].grid_height_range = (0.02, 0.14)
self.scene.terrain.terrain_generator.sub_terrains["pyramid_stairs"].step_height_range = (0.03, 0.16)
self.scene.terrain.terrain_generator.sub_terrains["pyramid_stairs_inv"].step_height_range = (0.03, 0.16)
self.scene.terrain.terrain_generator.sub_terrains["random_rough"].noise_range = (0.01, 0.10)
self.scene.terrain.terrain_generator.sub_terrains["random_rough"].noise_step = 0.01
# only randomize actuator gains for actuated joints (knees + wheels), exclude hipx/hipy completely
self.events.randomize_actuator_gains.params["asset_cfg"].joint_names = self.joint_names
# ------------------------------Rewards------------------------------
# General
self.rewards.is_terminated.weight = 0
# Root penalties
self.rewards.lin_vel_z_l2.weight = -2.0
self.rewards.ang_vel_xy_l2.weight = -0.02
self.rewards.flat_orientation_l2.weight = -0.5
self.rewards.base_height_l2.weight = -0.5
self.rewards.base_height_l2.params["target_height"] = 0.45
self.rewards.base_height_l2.params["asset_cfg"].body_names = [self.base_link_name]
self.rewards.body_lin_acc_l2.weight = 0
self.rewards.body_lin_acc_l2.params["asset_cfg"].body_names = [self.base_link_name]
# Joint penalties
self.rewards.joint_torques_l2.weight = -2.5e-5
self.rewards.joint_torques_l2.params["asset_cfg"].joint_names = self.leg_joint_names
self.rewards.joint_torques_wheel_l2.weight = 0
self.rewards.joint_torques_wheel_l2.params["asset_cfg"].joint_names = self.wheel_joint_names
self.rewards.joint_vel_l2.weight = 0
self.rewards.joint_vel_l2.params["asset_cfg"].joint_names = self.leg_joint_names
self.rewards.joint_vel_wheel_l2.weight = 0
self.rewards.joint_vel_wheel_l2.params["asset_cfg"].joint_names = self.wheel_joint_names
self.rewards.joint_acc_l2.weight = -2e-7
self.rewards.joint_acc_l2.params["asset_cfg"].joint_names = self.leg_joint_names
self.rewards.joint_acc_wheel_l2.weight = -1e-7
self.rewards.joint_acc_wheel_l2.params["asset_cfg"].joint_names = self.wheel_joint_names
# self.rewards.create_joint_deviation_l1_rewterm("joint_deviation_hip_l1", -0.2, [".*_hip_joint"])
self.rewards.joint_pos_limits.weight = -5.0
self.rewards.joint_pos_limits.params["asset_cfg"].joint_names = self.leg_joint_names
self.rewards.joint_vel_limits.weight = 0
self.rewards.joint_vel_limits.params["asset_cfg"].joint_names = self.wheel_joint_names
self.rewards.joint_power.weight = -2e-5
self.rewards.joint_power.params["asset_cfg"].joint_names = self.leg_joint_names
self.rewards.stand_still.weight = -2.0
self.rewards.stand_still.params["asset_cfg"].joint_names = self.leg_joint_names
self.rewards.knee_joint_pos_penalty.weight = -0.1
self.rewards.knee_joint_pos_penalty.params["asset_cfg"].joint_names = self.knee_joint_names
self.rewards.wheel_vel_penalty.weight = 0
self.rewards.wheel_vel_penalty.params["sensor_cfg"].body_names = self.foot_link_name
self.rewards.wheel_vel_penalty.params["asset_cfg"].joint_names = self.wheel_joint_names
self.rewards.joint_mirror.weight = -0.03
self.rewards.joint_mirror.params["mirror_joints"] = [
["fl_knee_joint", "hr_knee_joint"],
["fr_knee_joint", "hl_knee_joint"],
]
# Action penalties
self.rewards.action_rate_l2.weight = -0.01
# Contact sensor
self.rewards.undesired_contacts.weight = -1.0
self.rewards.undesired_contacts.params["sensor_cfg"].body_names = [f"^(?!.*{self.foot_link_name}).*"]
self.rewards.undesired_contacts.params["threshold"] = 1.0
self.rewards.contact_forces.weight = -1.5e-4
self.rewards.contact_forces.params["sensor_cfg"].body_names = [self.foot_link_name]
self.rewards.support_contact_progress.weight = 0.0
self.rewards.support_contact_progress.params["sensor_cfg"].body_names = self.support_link_names
self.rewards.support_contact_progress.params["wheel_sensor_cfg"].body_names = [self.foot_link_name]
self.rewards.support_contact_progress.params["wheel_asset_cfg"].joint_names = self.wheel_joint_names
self.rewards.support_contact_excess.weight = 0.0
self.rewards.support_contact_excess.params["sensor_cfg"].body_names = self.support_link_names
# Velocity-tracking rewards
self.rewards.track_lin_vel_xy_exp.weight = 2.0 # 1.8
self.rewards.track_ang_vel_z_exp.weight = 1.5 # 1.2
self.rewards.track_ang_vel_z_exp.params["std"] = 0.5
self.rewards.track_diff_wheel_yaw_exp.weight = 1.0
self.rewards.track_diff_wheel_yaw_exp.params["asset_cfg"].joint_names = self.wheel_joint_names
self.rewards.lin_vel_xy_yaw_command_l2.weight = -0.4
# Others
self.rewards.feet_air_time.weight = 0
self.rewards.feet_air_time.params["threshold"] = 0.5
self.rewards.feet_air_time.params["sensor_cfg"].body_names = [self.foot_link_name]
self.rewards.feet_contact.weight = 0
self.rewards.feet_contact.params["sensor_cfg"].body_names = [self.foot_link_name]
self.rewards.feet_contact_without_cmd.weight = 0.1
self.rewards.feet_contact_without_cmd.params["sensor_cfg"].body_names = [self.foot_link_name]
self.rewards.feet_stumble.weight = 0
self.rewards.feet_stumble.params["sensor_cfg"].body_names = [self.foot_link_name]
self.rewards.feet_slide.weight = 0
self.rewards.feet_slide.params["sensor_cfg"].body_names = [self.foot_link_name]
self.rewards.feet_slide.params["asset_cfg"].body_names = [self.foot_link_name]
self.rewards.feet_height.weight = 0
self.rewards.feet_height.params["target_height"] = 0.1
self.rewards.feet_height.params["asset_cfg"].body_names = [self.foot_link_name]
self.rewards.feet_height_body.weight = 0
self.rewards.feet_height_body.params["target_height"] = -0.4
self.rewards.feet_height_body.params["asset_cfg"].body_names = [self.foot_link_name]
self.rewards.feet_gait.weight = 0
self.rewards.feet_gait.params["synced_feet_pair_names"] = (("fl_wheel", "hr_wheel"), ("fr_wheel", "hl_wheel"))
self.rewards.upward.weight = 0.08
# If the weight of rewards is 0, set rewards to None
if self.__class__.__name__ == "DeeproboticsM20RoughEnvCfg":
self.disable_zero_weight_rewards()
# ------------------------------Terminations------------------------------
# self.terminations.illegal_contact.params["sensor_cfg"].body_names = [self.base_link_name]
self.terminations.illegal_contact = None
self.terminations.bad_orientation_2 = None
# ------------------------------Curriculums------------------------------
# self.curriculum.command_levels.params["range_multiplier"] = (0.2, 1.0)
self.curriculum.command_levels = None
# ------------------------------Commands------------------------------
self.commands.base_velocity.debug_vis = False
self.commands.base_velocity.ranges.lin_vel_x = (-2.0, 2.0)
self.commands.base_velocity.ranges.lin_vel_y = (0.0, 0.0)
self.commands.base_velocity.ranges.ang_vel_z = (-1.5, 1.5)
@configclass
class DeeproboticsM20SupportRoughEnvCfg(DeeproboticsM20RoughEnvCfg):
"""Stage 2: resume from Stage 1 and enable brief body/knee support for obstacle crossing."""
def __post_init__(self):
super().__post_init__()
# Keep the same policy/action dimensions as Stage 1 for checkpoint resume.
# Only rewards, command distribution, and terrain curriculum are changed here.
self.scene.terrain.max_init_terrain_level = 1
self.scene.terrain.terrain_generator.sub_terrains["boxes"].proportion = 0.25
self.scene.terrain.terrain_generator.sub_terrains["pyramid_stairs"].proportion = 0.25
self.scene.terrain.terrain_generator.sub_terrains["pyramid_stairs_inv"].proportion = 0.15
self.scene.terrain.terrain_generator.sub_terrains["random_rough"].proportion = 0.15
self.scene.terrain.terrain_generator.sub_terrains["hf_pyramid_slope"].proportion = 0.10
self.scene.terrain.terrain_generator.sub_terrains["hf_pyramid_slope_inv"].proportion = 0.10
self.scene.terrain.terrain_generator.sub_terrains["boxes"].grid_height_range = (0.03, 0.22)
self.scene.terrain.terrain_generator.sub_terrains["pyramid_stairs"].step_height_range = (0.04, 0.24)
self.scene.terrain.terrain_generator.sub_terrains["pyramid_stairs_inv"].step_height_range = (0.04, 0.20)
self.scene.terrain.terrain_generator.sub_terrains["random_rough"].noise_range = (0.01, 0.14)
# Keep privileged terrain information for the critic only. The actor remains blind/tactile.
self.observations.policy.height_scan = None
self.observations.critic.height_scan.params["sensor_cfg"] = SceneEntityCfg("height_scanner")
self.observations.critic.height_scan.clip = (-1.0, 1.0)
self.observations.critic.height_scan.scale = 1.0
# Body/knee contact is allowed only as short support during forward progress.
self.rewards.undesired_contacts.weight = -0.10
self.rewards.undesired_contacts.params["sensor_cfg"].body_names = [f"^(?!.*{self.foot_link_name}).*"]
self.rewards.undesired_contacts.params["threshold"] = 35.0
self.rewards.command_progress.weight = 0.8
self.rewards.support_contact_progress.weight = 1.0
self.rewards.support_contact_progress.params["sensor_cfg"].body_names = self.support_link_names
self.rewards.support_contact_progress.params["wheel_sensor_cfg"].body_names = [self.foot_link_name]
self.rewards.support_contact_progress.params["wheel_asset_cfg"].joint_names = self.wheel_joint_names
self.rewards.support_contact_excess.weight = -0.60
self.rewards.support_contact_excess.params["sensor_cfg"].body_names = self.support_link_names
self.rewards.support_contact_excess.params["force_threshold"] = 100.0
self.rewards.support_contact_excess.params["max_contact_time"] = 0.45
# Stage 2 should still avoid the bad solution of crawling on the body.
self.rewards.track_lin_vel_xy_exp.weight = 1.5
self.rewards.track_ang_vel_z_exp.weight = 1.0
self.rewards.flat_orientation_l2.weight = -0.45
self.rewards.base_height_l2.weight = -0.35
self.rewards.base_height_l2.params["target_height"] = 0.29
# Stage 2 first focuses on forward obstacle crossing, then expands the speed range.
self.commands.base_velocity.heading_command = False
self.commands.base_velocity.rel_standing_envs = 0.0
self.commands.base_velocity.rel_heading_envs = 0.0
self.commands.base_velocity.ranges.lin_vel_x = (0.25, 1.2)
self.commands.base_velocity.ranges.lin_vel_y = (0.0, 0.0)
self.commands.base_velocity.ranges.ang_vel_z = (-0.6, 0.6)
self.curriculum.command_levels = CurrTerm(
func=mdp.command_levels_positive_x,
params={
"reward_term_name": "track_lin_vel_xy_exp",
"initial_max_ratio": 0.35,
"increment": 0.1,
},
)
self.disable_zero_weight_rewards()
@@ -0,0 +1,21 @@
# Copyright (c) 2025 Deep Robotics
# SPDX-License-Identifier: BSD 3-Clause
#
# # Copyright (c) 2024-2025 Ziqi Fan
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) 2022-2025, The Isaac Lab Project Developers.
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
"""This sub-module contains the functions that are specific to the locomotion environments."""
from isaaclab.envs.mdp import * # noqa: F401, F403
from isaaclab_tasks.manager_based.locomotion.velocity.mdp import * # noqa: F401, F403
from .commands import * # noqa: F401, F403
from .curriculums import * # noqa: F401, F403
from .events import * # noqa: F401, F403
from .observations import * # noqa: F401, F403
from .rewards import * # noqa: F401, F403
@@ -0,0 +1,130 @@
# Copyright (c) 2025 Deep Robotics
# SPDX-License-Identifier: BSD 3-Clause
#
# # Copyright (c) 2024-2025 Ziqi Fan
# SPDX-License-Identifier: Apache-2.0
from __future__ import annotations
import torch
from typing import TYPE_CHECKING, Sequence
from isaaclab.managers import CommandTerm, CommandTermCfg
from isaaclab.utils import configclass
import rl_training.tasks.manager_based.locomotion.velocity.mdp as mdp
if TYPE_CHECKING:
from isaaclab.envs import ManagerBasedEnv
class UniformThresholdVelocityCommand(mdp.UniformVelocityCommand):
"""Command generator that generates a velocity command in SE(2) from uniform distribution with threshold."""
cfg: mdp.UniformThresholdVelocityCommandCfg
"""The configuration of the command generator."""
def _resample_command(self, env_ids: Sequence[int]):
super()._resample_command(env_ids)
# set small commands to zero
self.vel_command_b[env_ids, :2] *= (torch.norm(self.vel_command_b[env_ids, :2], dim=1) > 0.2).unsqueeze(1)
@configclass
class UniformThresholdVelocityCommandCfg(mdp.UniformVelocityCommandCfg):
"""Configuration for the uniform threshold velocity command generator."""
class_type: type = UniformThresholdVelocityCommand
class DiscreteCommandController(CommandTerm):
"""
Command generator that assigns discrete commands to environments.
Commands are stored as a list of predefined integers.
The controller maps these commands by their indices (e.g., index 0 -> 10, index 1 -> 20).
"""
cfg: DiscreteCommandControllerCfg
"""Configuration for the command controller."""
def __init__(self, cfg: DiscreteCommandControllerCfg, env: ManagerBasedEnv):
"""
Initialize the command controller.
Args:
cfg: The configuration of the command controller.
env: The environment object.
"""
# Initialize the base class
super().__init__(cfg, env)
# Validate that available_commands is non-empty
if not self.cfg.available_commands:
raise ValueError("The available_commands list cannot be empty.")
# Ensure all elements are integers
if not all(isinstance(cmd, int) for cmd in self.cfg.available_commands):
raise ValueError("All elements in available_commands must be integers.")
# Store the available commands
self.available_commands = self.cfg.available_commands
# Create buffers to store the command
# -- command buffer: stores discrete action indices for each environment
self.command_buffer = torch.zeros(self.num_envs, dtype=torch.int32, device=self.device)
# -- current_commands: stores a snapshot of the current commands (as integers)
self.current_commands = [self.available_commands[0]] * self.num_envs # Default to the first command
def __str__(self) -> str:
"""Return a string representation of the command controller."""
return (
"DiscreteCommandController:\n"
f"\tNumber of environments: {self.num_envs}\n"
f"\tAvailable commands: {self.available_commands}\n"
)
"""
Properties
"""
@property
def command(self) -> torch.Tensor:
"""Return the current command buffer. Shape is (num_envs, 1)."""
return self.command_buffer
"""
Implementation specific functions.
"""
def _update_metrics(self):
"""Update metrics for the command controller."""
pass
def _resample_command(self, env_ids: Sequence[int]):
"""Resample commands for the given environments."""
sampled_indices = torch.randint(
len(self.available_commands), (len(env_ids),), dtype=torch.int32, device=self.device
)
sampled_commands = torch.tensor(
[self.available_commands[idx.item()] for idx in sampled_indices], dtype=torch.int32, device=self.device
)
self.command_buffer[env_ids] = sampled_commands
def _update_command(self):
"""Update and store the current commands."""
self.current_commands = self.command_buffer.tolist()
@configclass
class DiscreteCommandControllerCfg(CommandTermCfg):
"""Configuration for the discrete command controller."""
class_type: type = DiscreteCommandController
available_commands: list[int] = []
"""
List of available discrete commands, where each element is an integer.
Example: [10, 20, 30, 40, 50]
"""
@@ -0,0 +1,95 @@
# Copyright (c) 2025 Deep Robotics
# SPDX-License-Identifier: BSD 3-Clause
#
# # Copyright (c) 2024-2025 Ziqi Fan
# SPDX-License-Identifier: Apache-2.0
"""Common functions that can be used to create curriculum for the learning environment.
The functions can be passed to the :class:`isaaclab.managers.CurriculumTermCfg` object to enable
the curriculum introduced by the function.
"""
from __future__ import annotations
import torch
from collections.abc import Sequence
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from isaaclab.envs import ManagerBasedRLEnv
def command_levels_vel(
env: ManagerBasedRLEnv,
env_ids: Sequence[int],
reward_term_name: str,
range_multiplier: Sequence[float] = (0.1, 1.0),
) -> None:
"""command_levels_vel"""
base_velocity_ranges = env.command_manager.get_term("base_velocity").cfg.ranges
# Get original velocity ranges (ONLY ON FIRST EPISODE)
if env.common_step_counter == 0:
env._original_vel_x = torch.tensor(base_velocity_ranges.lin_vel_x, device=env.device)
env._original_vel_y = torch.tensor(base_velocity_ranges.lin_vel_y, device=env.device)
env._initial_vel_x = env._original_vel_x * range_multiplier[0]
env._final_vel_x = env._original_vel_x * range_multiplier[1]
env._initial_vel_y = env._original_vel_y * range_multiplier[0]
env._final_vel_y = env._original_vel_y * range_multiplier[1]
# Initialize command ranges to initial values
base_velocity_ranges.lin_vel_x = env._initial_vel_x.tolist()
base_velocity_ranges.lin_vel_y = env._initial_vel_y.tolist()
# avoid updating command curriculum at each step since the maximum command is common to all envs
if env.common_step_counter % env.max_episode_length == 0:
episode_sums = env.reward_manager._episode_sums[reward_term_name]
reward_term_cfg = env.reward_manager.get_term_cfg(reward_term_name)
delta_command = torch.tensor([-0.1, 0.1], device=env.device)
# If the tracking reward is above 80% of the maximum, increase the range of commands
if torch.mean(episode_sums[env_ids]) / env.max_episode_length_s > 0.8 * reward_term_cfg.weight:
new_vel_x = torch.tensor(base_velocity_ranges.lin_vel_x, device=env.device) + delta_command
new_vel_y = torch.tensor(base_velocity_ranges.lin_vel_y, device=env.device) + delta_command
# Clamp to ensure we don't exceed final ranges
new_vel_x = torch.clamp(new_vel_x, min=env._final_vel_x[0], max=env._final_vel_x[1])
new_vel_y = torch.clamp(new_vel_y, min=env._final_vel_y[0], max=env._final_vel_y[1])
# Update ranges
base_velocity_ranges.lin_vel_x = new_vel_x.tolist()
base_velocity_ranges.lin_vel_y = new_vel_y.tolist()
return torch.tensor(base_velocity_ranges.lin_vel_x[1], device=env.device)
def command_levels_positive_x(
env: ManagerBasedRLEnv,
env_ids: Sequence[int],
reward_term_name: str,
initial_max_ratio: float = 0.35,
increment: float = 0.1,
) -> torch.Tensor:
"""Expand only the upper bound of a positive forward-speed command range.
Unlike :func:`command_levels_vel`, this curriculum preserves the configured minimum forward speed. This avoids
producing near-zero commands in obstacle-focused stages where the policy needs enough momentum to learn climbing.
"""
base_velocity_ranges = env.command_manager.get_term("base_velocity").cfg.ranges
if env.common_step_counter == 0:
original_x = torch.tensor(base_velocity_ranges.lin_vel_x, device=env.device)
env._positive_x_min = original_x[0]
env._positive_x_max = original_x[1]
initial_x_max = torch.maximum(env._positive_x_min, env._positive_x_max * initial_max_ratio)
base_velocity_ranges.lin_vel_x = [env._positive_x_min.item(), initial_x_max.item()]
if env.common_step_counter % env.max_episode_length == 0:
episode_sums = env.reward_manager._episode_sums[reward_term_name]
reward_term_cfg = env.reward_manager.get_term_cfg(reward_term_name)
mean_reward = torch.mean(episode_sums[env_ids]) / env.max_episode_length_s
if mean_reward > 0.8 * reward_term_cfg.weight:
current_x_max = torch.tensor(base_velocity_ranges.lin_vel_x[1], device=env.device)
new_x_max = torch.clamp(current_x_max + increment, max=env._positive_x_max)
base_velocity_ranges.lin_vel_x = [env._positive_x_min.item(), new_x_max.item()]
return torch.tensor(base_velocity_ranges.lin_vel_x[1], device=env.device)
@@ -0,0 +1,213 @@
# Copyright (c) 2025 Deep Robotics
# SPDX-License-Identifier: BSD 3-Clause
#
# # Copyright (c) 2024-2025 Ziqi Fan
# SPDX-License-Identifier: Apache-2.0
from __future__ import annotations
import torch
from typing import TYPE_CHECKING, Literal
import isaaclab.utils.math as math_utils
from isaaclab.assets import Articulation, RigidObject
from isaaclab.managers import SceneEntityCfg
if TYPE_CHECKING:
from isaaclab.envs import ManagerBasedEnv
def randomize_rigid_body_inertia(
env: ManagerBasedEnv,
env_ids: torch.Tensor | None,
asset_cfg: SceneEntityCfg,
inertia_distribution_params: tuple[float, float],
operation: Literal["add", "scale", "abs"],
distribution: Literal["uniform", "log_uniform", "gaussian"] = "uniform",
):
"""Randomize the inertia tensors of the bodies by adding, scaling, or setting random values.
This function allows randomizing only the diagonal inertia tensor components (xx, yy, zz) of the bodies.
The function samples random values from the given distribution parameters and adds, scales, or sets the values
into the physics simulation based on the operation.
.. tip::
This function uses CPU tensors to assign the body inertias. It is recommended to use this function
only during the initialization of the environment.
"""
# extract the used quantities (to enable type-hinting)
asset: RigidObject | Articulation = env.scene[asset_cfg.name]
# resolve environment ids
if env_ids is None:
env_ids = torch.arange(env.scene.num_envs, device="cpu")
else:
env_ids = env_ids.cpu()
# resolve body indices
if asset_cfg.body_ids == slice(None):
body_ids = torch.arange(asset.num_bodies, dtype=torch.int, device="cpu")
else:
body_ids = torch.tensor(asset_cfg.body_ids, dtype=torch.int, device="cpu")
# get the current inertia tensors of the bodies (num_assets, num_bodies, 9 for articulations or 9 for rigid objects)
inertias = asset.root_physx_view.get_inertias()
# apply randomization on default values
inertias[env_ids[:, None], body_ids, :] = asset.data.default_inertia[env_ids[:, None], body_ids, :].clone()
# randomize each diagonal element (xx, yy, zz -> indices 0, 4, 8)
for idx in [0, 4, 8]:
# Extract and randomize the specific diagonal element
randomized_inertias = _randomize_prop_by_op(
inertias[:, :, idx],
inertia_distribution_params,
env_ids,
body_ids,
operation,
distribution,
)
# Assign the randomized values back to the inertia tensor
inertias[env_ids[:, None], body_ids, idx] = randomized_inertias
# set the inertia tensors into the physics simulation
asset.root_physx_view.set_inertias(inertias, env_ids)
def randomize_com_positions(
env: ManagerBasedEnv,
env_ids: torch.Tensor | None,
asset_cfg: SceneEntityCfg,
com_distribution_params: tuple[float, float],
operation: Literal["add", "scale", "abs"],
distribution: Literal["uniform", "log_uniform", "gaussian"] = "uniform",
):
"""Randomize the center of mass (COM) positions for the rigid bodies.
This function allows randomizing the COM positions of the bodies in the physics simulation. The positions can be
randomized by adding, scaling, or setting random values sampled from the specified distribution.
.. tip::
This function is intended for initialization or offline adjustments, as it modifies physics properties directly.
Args:
env (ManagerBasedEnv): The simulation environment.
env_ids (torch.Tensor | None): Specific environment indices to apply randomization, or None for all environments.
asset_cfg (SceneEntityCfg): The configuration for the target asset whose COM will be randomized.
com_distribution_params (tuple[float, float]): Parameters of the distribution (e.g., min and max for uniform).
operation (Literal["add", "scale", "abs"]): The operation to apply for randomization.
distribution (Literal["uniform", "log_uniform", "gaussian"]): The distribution to sample random values from.
"""
# Extract the asset (Articulation or RigidObject)
asset: RigidObject | Articulation = env.scene[asset_cfg.name]
# Resolve environment indices
if env_ids is None:
env_ids = torch.arange(env.scene.num_envs, device="cpu")
else:
env_ids = env_ids.cpu()
# Resolve body indices
if asset_cfg.body_ids == slice(None):
body_ids = torch.arange(asset.num_bodies, dtype=torch.int, device="cpu")
else:
body_ids = torch.tensor(asset_cfg.body_ids, dtype=torch.int, device="cpu")
# Get the current COM offsets (num_assets, num_bodies, 3)
com_offsets = asset.root_physx_view.get_coms()
for dim_idx in range(3): # Randomize x, y, z independently
randomized_offset = _randomize_prop_by_op(
com_offsets[:, :, dim_idx],
com_distribution_params,
env_ids,
body_ids,
operation,
distribution,
)
com_offsets[env_ids[:, None], body_ids, dim_idx] = randomized_offset[env_ids[:, None], body_ids]
# Set the randomized COM offsets into the simulation
asset.root_physx_view.set_coms(com_offsets, env_ids)
"""
Internal helper functions.
"""
def _randomize_prop_by_op(
data: torch.Tensor,
distribution_parameters: tuple[float | torch.Tensor, float | torch.Tensor],
dim_0_ids: torch.Tensor | None,
dim_1_ids: torch.Tensor | slice,
operation: Literal["add", "scale", "abs"],
distribution: Literal["uniform", "log_uniform", "gaussian"],
) -> torch.Tensor:
"""Perform data randomization based on the given operation and distribution.
Args:
data: The data tensor to be randomized. Shape is (dim_0, dim_1).
distribution_parameters: The parameters for the distribution to sample values from.
dim_0_ids: The indices of the first dimension to randomize.
dim_1_ids: The indices of the second dimension to randomize.
operation: The operation to perform on the data. Options: 'add', 'scale', 'abs'.
distribution: The distribution to sample the random values from. Options: 'uniform', 'log_uniform'.
Returns:
The data tensor after randomization. Shape is (dim_0, dim_1).
Raises:
NotImplementedError: If the operation or distribution is not supported.
"""
# resolve shape
# -- dim 0
if dim_0_ids is None:
n_dim_0 = data.shape[0]
dim_0_ids = slice(None) # type: ignore
else:
n_dim_0 = len(dim_0_ids)
if not isinstance(dim_1_ids, slice):
dim_0_ids = dim_0_ids[:, None]
# -- dim 1
if isinstance(dim_1_ids, slice):
n_dim_1 = data.shape[1]
else:
n_dim_1 = len(dim_1_ids)
# resolve the distribution
if distribution == "uniform":
dist_fn = math_utils.sample_uniform
elif distribution == "log_uniform":
dist_fn = math_utils.sample_log_uniform
elif distribution == "gaussian":
dist_fn = math_utils.sample_gaussian
else:
raise NotImplementedError(
f"Unknown distribution: '{distribution}' for joint properties randomization."
" Please use 'uniform', 'log_uniform', 'gaussian'."
)
# perform the operation
if operation == "add":
data[dim_0_ids, dim_1_ids] += dist_fn(*distribution_parameters, (n_dim_0, n_dim_1), device=data.device)
elif operation == "scale":
data[dim_0_ids, dim_1_ids] *= dist_fn(*distribution_parameters, (n_dim_0, n_dim_1), device=data.device)
elif operation == "abs":
data[dim_0_ids, dim_1_ids] = dist_fn(*distribution_parameters, (n_dim_0, n_dim_1), device=data.device)
else:
raise NotImplementedError(
f"Unknown operation: '{operation}' for property randomization. Please use 'add', 'scale', or 'abs'."
)
return data
def bad_orientation_2(
env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot") # type: ignore
) -> torch.Tensor:
"""Terminate when the asset's orientation is too far from the desired orientation limits.
This is computed by checking the angle between the projected gravity vector and the z-axis.
"""
# extract the used quantities (to enable type-hinting)
asset: RigidObject = env.scene[asset_cfg.name]
return (asset.data.projected_gravity_b[:, 2] > 0) | (asset.data.projected_gravity_b[:, :2].abs() > 0.7).any(-1)
@@ -0,0 +1,105 @@
# Copyright (c) 2025 Deep Robotics
# SPDX-License-Identifier: BSD 3-Clause
#
# # Copyright (c) 2024-2025 Ziqi Fan
# SPDX-License-Identifier: Apache-2.0
from __future__ import annotations
import torch
from typing import TYPE_CHECKING
from isaaclab.assets import Articulation
from isaaclab.managers import SceneEntityCfg
from isaaclab.sensors import ContactSensor
if TYPE_CHECKING:
from isaaclab.envs import ManagerBasedEnv, ManagerBasedRLEnv
# def joint_pos_rel_without_wheel(
# env: ManagerBasedEnv,
# asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"),
# wheel_asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"),
# ) -> torch.Tensor:
# """The joint positions of the asset w.r.t. the default joint positions.(Without the wheel joints)"""
# # extract the used quantities (to enable type-hinting)
# asset: Articulation = env.scene[asset_cfg.name]
# joint_pos_rel = asset.data.joint_pos[:, asset_cfg.joint_ids] - asset.data.default_joint_pos[:, asset_cfg.joint_ids]
# joint_pos_rel[:, wheel_asset_cfg.joint_ids] = 0
# return joint_pos_rel
def joint_pos_rel_without_wheel(
env: ManagerBasedEnv,
asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"),
wheel_asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"),
) -> torch.Tensor:
asset: Articulation = env.scene[asset_cfg.name]
# 1. 先在“完整 joint 空间”算相对位置
joint_pos_rel_full = asset.data.joint_pos - asset.data.default_joint_pos
# 2. 把 wheel joint 清零(这里的 index 是全局的,合法)
joint_pos_rel_full[:, wheel_asset_cfg.joint_ids] = 0.0
# 3. 最后再裁剪成 policy 需要的 joint
joint_pos_rel = joint_pos_rel_full[:, asset_cfg.joint_ids]
return joint_pos_rel
def phase(env: ManagerBasedRLEnv, cycle_time: float) -> torch.Tensor:
if not hasattr(env, "episode_length_buf") or env.episode_length_buf is None:
env.episode_length_buf = torch.zeros(env.num_envs, device=env.device, dtype=torch.long)
phase = env.episode_length_buf[:, None] * env.step_dt / cycle_time
phase_tensor = torch.cat([torch.sin(2 * torch.pi * phase), torch.cos(2 * torch.pi * phase)], dim=-1)
return phase_tensor
def contact_state(
env: ManagerBasedRLEnv,
sensor_cfg: SceneEntityCfg,
threshold: float = 1.0,
) -> torch.Tensor:
"""Binary contact state for selected bodies."""
contact_sensor: ContactSensor = env.scene.sensors[sensor_cfg.name]
force_norm = torch.linalg.norm(contact_sensor.data.net_forces_w[:, sensor_cfg.body_ids, :], dim=-1)
return (force_norm > threshold).float()
def contact_force_norm(
env: ManagerBasedRLEnv,
sensor_cfg: SceneEntityCfg,
scale: float = 100.0,
clip: float = 2.0,
) -> torch.Tensor:
"""Normalized contact force magnitude for selected bodies."""
contact_sensor: ContactSensor = env.scene.sensors[sensor_cfg.name]
force_norm = torch.linalg.norm(contact_sensor.data.net_forces_w[:, sensor_cfg.body_ids, :], dim=-1)
return torch.clamp(force_norm / scale, min=0.0, max=clip)
def wheel_slip_proxy(
env: ManagerBasedRLEnv,
wheel_radius: float,
asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"),
) -> torch.Tensor:
"""A proprioceptive slip proxy from wheel peripheral speed and base forward speed."""
asset: Articulation = env.scene[asset_cfg.name]
wheel_speed = torch.abs(asset.data.joint_vel[:, asset_cfg.joint_ids] * wheel_radius)
base_forward_speed = torch.abs(asset.data.root_lin_vel_b[:, 0:1])
return torch.clamp(torch.abs(wheel_speed - base_forward_speed), min=0.0, max=3.0)
def applied_torque(
env: ManagerBasedRLEnv,
asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"),
scale: float = 80.0,
clip: float = 2.0,
) -> torch.Tensor:
"""Normalized applied motor torques."""
asset: Articulation = env.scene[asset_cfg.name]
torque = asset.data.applied_torque[:, asset_cfg.joint_ids]
return torch.clamp(torque / scale, min=-clip, max=clip)
@@ -0,0 +1,985 @@
# Copyright (c) 2025 Deep Robotics
# SPDX-License-Identifier: BSD 3-Clause
# Copyright (c) 2024-2025 Ziqi Fan
# SPDX-License-Identifier: Apache-2.0
from __future__ import annotations
import torch
from typing import TYPE_CHECKING
import isaaclab.utils.math as math_utils
from isaaclab.assets import Articulation, RigidObject
from isaaclab.managers import ManagerTermBase
from isaaclab.managers import RewardTermCfg as RewTerm
from isaaclab.managers import SceneEntityCfg
from isaaclab.sensors import ContactSensor, RayCaster
from isaaclab.utils.math import quat_apply_inverse, yaw_quat
if TYPE_CHECKING:
from isaaclab.envs import ManagerBasedRLEnv
def track_lin_vel_xy_exp(
env: ManagerBasedRLEnv, std: float, command_name: str, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot")
) -> torch.Tensor:
"""Reward tracking of linear velocity commands (xy axes) using exponential kernel."""
# extract the used quantities (to enable type-hinting)
asset: RigidObject = env.scene[asset_cfg.name]
# compute the error
lin_vel_error = torch.sum(
torch.square(env.command_manager.get_command(command_name)[:, :2] - asset.data.root_lin_vel_b[:, :2]),
dim=1,
)
reward = torch.exp(-lin_vel_error / std**2)
# reward *= torch.clamp(-env.scene["robot"].data.projected_gravity_b[:, 2], 0, 0.7) / 0.7
return reward
def track_ang_vel_z_exp(
env: ManagerBasedRLEnv, std: float, command_name: str, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot")
) -> torch.Tensor:
"""Reward tracking of angular velocity commands (yaw) using exponential kernel."""
# extract the used quantities (to enable type-hinting)
asset: RigidObject = env.scene[asset_cfg.name]
# compute the error
ang_vel_error = torch.square(env.command_manager.get_command(command_name)[:, 2] - asset.data.root_ang_vel_b[:, 2])
reward = torch.exp(-ang_vel_error / std**2)
# reward *= torch.clamp(-env.scene["robot"].data.projected_gravity_b[:, 2], 0, 0.7) / 0.7
return reward
def track_diff_wheel_yaw_exp(
env: ManagerBasedRLEnv,
std: float,
command_name: str,
wheel_radius: float,
track_width: float,
left_wheel_indices: tuple[int, ...],
right_wheel_indices: tuple[int, ...],
command_threshold: float,
asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"),
) -> torch.Tensor:
"""Reward differential wheel speed matching the commanded yaw rate."""
asset: Articulation = env.scene[asset_cfg.name]
joint_vel = asset.data.joint_vel[:, asset_cfg.joint_ids]
left_wheel_vel = torch.mean(joint_vel[:, left_wheel_indices], dim=1)
right_wheel_vel = torch.mean(joint_vel[:, right_wheel_indices], dim=1)
estimated_yaw_rate = wheel_radius * (right_wheel_vel - left_wheel_vel) / track_width
yaw_command = env.command_manager.get_command(command_name)[:, 2]
yaw_error = torch.square(yaw_command - estimated_yaw_rate)
reward = torch.exp(-yaw_error / std**2)
return torch.where(torch.abs(yaw_command) > command_threshold, reward, torch.zeros_like(reward))
def lin_vel_xy_yaw_command_l2(
env: ManagerBasedRLEnv,
command_name: str,
yaw_command_threshold: float,
lin_command_threshold: float,
asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"),
) -> torch.Tensor:
"""Penalize linear drift when the command asks mostly for in-place yaw."""
asset: Articulation = env.scene[asset_cfg.name]
command = env.command_manager.get_command(command_name)
is_turn_in_place = (
torch.abs(command[:, 2]) > yaw_command_threshold
) & (torch.linalg.norm(command[:, :2], dim=1) < lin_command_threshold)
lin_vel_error = torch.sum(torch.square(asset.data.root_lin_vel_b[:, :2]), dim=1)
return torch.where(is_turn_in_place, lin_vel_error, torch.zeros_like(lin_vel_error))
def command_progress(
env: ManagerBasedRLEnv,
command_name: str,
min_command: float,
max_reward_speed: float,
asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"),
) -> torch.Tensor:
"""Reward positive progress along the commanded planar direction."""
asset: Articulation = env.scene[asset_cfg.name]
command_xy = env.command_manager.get_command(command_name)[:, :2]
command_norm = torch.linalg.norm(command_xy, dim=1)
command_dir = command_xy / torch.clamp(command_norm.unsqueeze(1), min=1e-6)
progress_speed = torch.sum(asset.data.root_lin_vel_b[:, :2] * command_dir, dim=1)
reward = torch.clamp(progress_speed, min=0.0, max=max_reward_speed) / max_reward_speed
return torch.where(command_norm > min_command, reward, torch.zeros_like(reward))
def support_contact_excess(
env: ManagerBasedRLEnv,
sensor_cfg: SceneEntityCfg,
force_threshold: float = 120.0,
max_contact_time: float = 0.6,
) -> torch.Tensor:
"""Penalize dragging or high-force support contacts."""
contact_sensor: ContactSensor = env.scene.sensors[sensor_cfg.name]
force_norm = torch.linalg.norm(contact_sensor.data.net_forces_w[:, sensor_cfg.body_ids, :], dim=-1)
contact_time = contact_sensor.data.current_contact_time[:, sensor_cfg.body_ids]
force_excess = torch.clamp(force_norm - force_threshold, min=0.0) / force_threshold
time_excess = torch.clamp(contact_time - max_contact_time, min=0.0)
return torch.sum(force_excess + time_excess, dim=1)
class ObstacleSupportEscapeReward(ManagerTermBase):
"""Reward a blocked -> brief support contact -> recovered progress sequence.
The state machine prevents ordinary forward motion with incidental support contact from receiving the full reward.
Obstacle evidence is inferred from low commanded progress together with either horizontal wheel force or wheel slip,
so the actor can remain blind while the training reward uses contact information already available on the robot.
"""
def __init__(self, cfg: RewTerm, env: ManagerBasedRLEnv):
super().__init__(cfg, env)
self._blocked_memory = torch.zeros(self.num_envs, device=self.device)
self._support_window = torch.zeros(self.num_envs, device=self.device)
self._recovery_reward_window = torch.zeros(self.num_envs, device=self.device)
self._cooldown = torch.zeros(self.num_envs, device=self.device)
self._previous_support_active = torch.zeros(self.num_envs, dtype=torch.bool, device=self.device)
def reset(self, env_ids=None) -> None:
if env_ids is None:
env_ids = slice(None)
self._blocked_memory[env_ids] = 0.0
self._support_window[env_ids] = 0.0
self._recovery_reward_window[env_ids] = 0.0
self._cooldown[env_ids] = 0.0
self._previous_support_active[env_ids] = False
def __call__(
self,
env: ManagerBasedRLEnv,
command_name: str,
sensor_cfg: SceneEntityCfg,
wheel_sensor_cfg: SceneEntityCfg,
asset_cfg: SceneEntityCfg,
wheel_asset_cfg: SceneEntityCfg,
wheel_radius: float,
support_force_threshold: float,
wheel_obstacle_force_threshold: float,
max_contact_time: float,
min_command: float,
stall_speed_ratio: float,
min_stall_speed: float,
slip_threshold: float,
obstacle_memory_time: float,
support_window_time: float,
recovery_speed_ratio: float,
min_recovery_speed: float,
recovery_reward_time: float,
cooldown_time: float,
recovery_bonus: float,
max_reward_speed: float,
) -> torch.Tensor:
asset: Articulation = env.scene[asset_cfg.name]
contact_sensor: ContactSensor = env.scene.sensors[sensor_cfg.name]
dt = float(env.step_dt)
command_x = env.command_manager.get_command(command_name)[:, 0]
command_speed = torch.abs(command_x)
direction = torch.where(command_x >= 0.0, torch.ones_like(command_x), -torch.ones_like(command_x))
progress_speed = direction * asset.data.root_lin_vel_b[:, 0]
commanded_motion = command_speed > min_command
wheel_forces = contact_sensor.data.net_forces_w[:, wheel_sensor_cfg.body_ids, :]
max_horizontal_wheel_force = torch.linalg.norm(wheel_forces[..., :2], dim=-1).amax(dim=1)
wheel_joint_vel = asset.data.joint_vel[:, wheel_asset_cfg.joint_ids]
mean_wheel_surface_speed = torch.mean(torch.abs(wheel_joint_vel), dim=1) * wheel_radius
wheel_slip = torch.clamp(mean_wheel_surface_speed - torch.clamp(progress_speed, min=0.0), min=0.0)
stall_limit = torch.maximum(stall_speed_ratio * command_speed, torch.full_like(command_speed, min_stall_speed))
obstacle_evidence = commanded_motion & (progress_speed < stall_limit) & (
(max_horizontal_wheel_force > wheel_obstacle_force_threshold) | (wheel_slip > slip_threshold)
)
self._blocked_memory = torch.clamp(self._blocked_memory - dt, min=0.0)
self._support_window = torch.clamp(self._support_window - dt, min=0.0)
self._recovery_reward_window = torch.clamp(self._recovery_reward_window - dt, min=0.0)
self._cooldown = torch.clamp(self._cooldown - dt, min=0.0)
self._blocked_memory = torch.where(
obstacle_evidence, torch.full_like(self._blocked_memory, obstacle_memory_time), self._blocked_memory
)
support_force = torch.linalg.norm(contact_sensor.data.net_forces_w[:, sensor_cfg.body_ids, :], dim=-1)
support_time = contact_sensor.data.current_contact_time[:, sensor_cfg.body_ids]
support_active = torch.any(support_force > support_force_threshold, dim=1)
brief_support = torch.any(
(support_force > support_force_threshold) & (support_time <= max_contact_time), dim=1
)
support_onset = brief_support & ~self._previous_support_active
valid_support_onset = support_onset & (self._blocked_memory > 0.0) & (self._cooldown <= 0.0)
was_waiting_for_recovery = self._support_window > 0.0
self._support_window = torch.where(
valid_support_onset, torch.full_like(self._support_window, support_window_time), self._support_window
)
recovery_threshold = torch.maximum(
recovery_speed_ratio * command_speed, torch.full_like(command_speed, min_recovery_speed)
)
excessive_support = torch.any(
(support_force > support_force_threshold) & (support_time > max_contact_time), dim=1
)
recovered = (
was_waiting_for_recovery
& commanded_motion
& (progress_speed >= recovery_threshold)
& ~excessive_support
)
self._recovery_reward_window = torch.where(
recovered,
torch.full_like(self._recovery_reward_window, recovery_reward_time),
self._recovery_reward_window,
)
self._support_window = torch.where(recovered, torch.zeros_like(self._support_window), self._support_window)
self._cooldown = torch.where(recovered, torch.full_like(self._cooldown, cooldown_time), self._cooldown)
recovery_progress = torch.clamp(progress_speed, min=0.0, max=max_reward_speed) / max_reward_speed
# Delay the discrete bonus until recovery is observed. This makes support contact alone worthless and
# prevents the policy from farming reward by repeatedly tapping the body against an obstacle.
reward = recovery_bonus * recovered.float() / dt
reward += torch.where(
self._recovery_reward_window > 0.0, recovery_progress, torch.zeros_like(recovery_progress)
)
self._previous_support_active = support_active
return reward
def track_lin_vel_xy_yaw_frame_exp(
env, std: float, command_name: str, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot")
) -> torch.Tensor:
"""Reward tracking of linear velocity commands (xy axes) in the gravity aligned robot frame using exponential kernel."""
# extract the used quantities (to enable type-hinting)
asset = env.scene[asset_cfg.name]
vel_yaw = quat_apply_inverse(yaw_quat(asset.data.root_quat_w), asset.data.root_lin_vel_w[:, :3])
lin_vel_error = torch.sum(
torch.square(env.command_manager.get_command(command_name)[:, :2] - vel_yaw[:, :2]), dim=1
)
reward = torch.exp(-lin_vel_error / std**2)
# reward *= torch.clamp(-env.scene["robot"].data.projected_gravity_b[:, 2], 0, 0.7) / 0.7
return reward
def track_ang_vel_z_world_exp(
env, command_name: str, std: float, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot")
) -> torch.Tensor:
"""Reward tracking of angular velocity commands (yaw) in world frame using exponential kernel."""
# extract the used quantities (to enable type-hinting)
asset = env.scene[asset_cfg.name]
ang_vel_error = torch.square(env.command_manager.get_command(command_name)[:, 2] - asset.data.root_ang_vel_w[:, 2])
reward = torch.exp(-ang_vel_error / std**2)
reward *= torch.clamp(-env.scene["robot"].data.projected_gravity_b[:, 2], 0, 0.7) / 0.7
return reward
def joint_power(env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot")) -> torch.Tensor:
"""Reward joint_power"""
# extract the used quantities (to enable type-hinting)
asset: Articulation = env.scene[asset_cfg.name]
# compute the reward
reward = torch.sum(
torch.abs(asset.data.joint_vel[:, asset_cfg.joint_ids] * asset.data.applied_torque[:, asset_cfg.joint_ids]),
dim=1,
)
return reward
def stand_still_without_cmd(
env: ManagerBasedRLEnv,
command_name: str,
command_threshold: float,
asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"),
) -> torch.Tensor:
"""Penalize joint positions that deviate from the default one when no command."""
# extract the used quantities (to enable type-hinting)
asset: Articulation = env.scene[asset_cfg.name]
# compute out of limits constraints
diff_angle = asset.data.joint_pos[:, asset_cfg.joint_ids] - asset.data.default_joint_pos[:, asset_cfg.joint_ids]
reward = torch.sum(torch.abs(diff_angle), dim=1)
reward *= torch.linalg.norm(env.command_manager.get_command(command_name), dim=1) < command_threshold
# reward *= torch.clamp(-env.scene["robot"].data.projected_gravity_b[:, 2], 0, 0.7) / 0.7
return reward
def joint_pos_penalty(
env: ManagerBasedRLEnv,
command_name: str,
asset_cfg: SceneEntityCfg,
stand_still_scale: float,
velocity_threshold: float,
command_threshold: float,
) -> torch.Tensor:
"""Penalize joint position error from default on the articulation."""
# extract the used quantities (to enable type-hinting)
asset: Articulation = env.scene[asset_cfg.name]
cmd = torch.linalg.norm(env.command_manager.get_command(command_name), dim=1)
body_vel = torch.linalg.norm(asset.data.root_lin_vel_b[:, :2], dim=1)
running_reward = torch.linalg.norm(
(asset.data.joint_pos[:, asset_cfg.joint_ids] - asset.data.default_joint_pos[:, asset_cfg.joint_ids]), dim=1
)
reward = torch.where(
torch.logical_or(cmd > command_threshold, body_vel > velocity_threshold),
running_reward,
stand_still_scale * running_reward,
)
# reward *= torch.clamp(-env.scene["robot"].data.projected_gravity_b[:, 2], 0, 0.7) / 0.7
return reward
def wheel_vel_penalty(
env: ManagerBasedRLEnv,
sensor_cfg: SceneEntityCfg,
command_name: str,
velocity_threshold: float,
command_threshold: float,
asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"),
) -> torch.Tensor:
asset: Articulation = env.scene[asset_cfg.name]
cmd = torch.linalg.norm(env.command_manager.get_command(command_name), dim=1)
body_vel = torch.linalg.norm(asset.data.root_lin_vel_b[:, :2], dim=1)
joint_vel = torch.abs(asset.data.joint_vel[:, asset_cfg.joint_ids])
contact_sensor: ContactSensor = env.scene.sensors[sensor_cfg.name]
in_air = contact_sensor.compute_first_air(env.step_dt)[:, sensor_cfg.body_ids]
running_reward = torch.sum(in_air * joint_vel, dim=1)
standing_reward = torch.sum(joint_vel, dim=1)
reward = torch.where(
torch.logical_or(cmd > command_threshold, body_vel > velocity_threshold),
running_reward,
standing_reward,
)
return reward
class GaitReward(ManagerTermBase):
"""Gait enforcing reward term for quadrupeds.
This reward penalizes contact timing differences between selected foot pairs defined in :attr:`synced_feet_pair_names`
to bias the policy towards a desired gait, i.e trotting, bounding, or pacing. Note that this reward is only for
quadrupedal gaits with two pairs of synchronized feet.
"""
def __init__(self, cfg: RewTerm, env: ManagerBasedRLEnv):
"""Initialize the term.
Args:
cfg: The configuration of the reward.
env: The RL environment instance.
"""
super().__init__(cfg, env)
self.std: float = cfg.params["std"]
self.command_name: str = cfg.params["command_name"]
self.max_err: float = cfg.params["max_err"]
self.velocity_threshold: float = cfg.params["velocity_threshold"]
self.command_threshold: float = cfg.params["command_threshold"]
self.contact_sensor: ContactSensor = env.scene.sensors[cfg.params["sensor_cfg"].name]
self.asset: Articulation = env.scene[cfg.params["asset_cfg"].name]
# match foot body names with corresponding foot body ids
synced_feet_pair_names = cfg.params["synced_feet_pair_names"]
if (
len(synced_feet_pair_names) != 2
or len(synced_feet_pair_names[0]) != 2
or len(synced_feet_pair_names[1]) != 2
):
raise ValueError("This reward only supports gaits with two pairs of synchronized feet, like trotting.")
synced_feet_pair_0 = self.contact_sensor.find_bodies(synced_feet_pair_names[0])[0]
synced_feet_pair_1 = self.contact_sensor.find_bodies(synced_feet_pair_names[1])[0]
self.synced_feet_pairs = [synced_feet_pair_0, synced_feet_pair_1]
def __call__(
self,
env: ManagerBasedRLEnv,
std: float,
command_name: str,
max_err: float,
velocity_threshold: float,
command_threshold: float,
synced_feet_pair_names,
asset_cfg: SceneEntityCfg,
sensor_cfg: SceneEntityCfg,
) -> torch.Tensor:
"""Compute the reward.
This reward is defined as a multiplication between six terms where two of them enforce pair feet
being in sync and the other four rewards if all the other remaining pairs are out of sync
Args:
env: The RL environment instance.
Returns:
The reward value.
"""
# for synchronous feet, the contact (air) times of two feet should match
sync_reward_0 = self._sync_reward_func(self.synced_feet_pairs[0][0], self.synced_feet_pairs[0][1])
sync_reward_1 = self._sync_reward_func(self.synced_feet_pairs[1][0], self.synced_feet_pairs[1][1])
sync_reward = sync_reward_0 * sync_reward_1
# for asynchronous feet, the contact time of one foot should match the air time of the other one
async_reward_0 = self._async_reward_func(self.synced_feet_pairs[0][0], self.synced_feet_pairs[1][0])
async_reward_1 = self._async_reward_func(self.synced_feet_pairs[0][1], self.synced_feet_pairs[1][1])
async_reward_2 = self._async_reward_func(self.synced_feet_pairs[0][0], self.synced_feet_pairs[1][1])
async_reward_3 = self._async_reward_func(self.synced_feet_pairs[1][0], self.synced_feet_pairs[0][1])
async_reward = async_reward_0 * async_reward_1 * async_reward_2 * async_reward_3
# only enforce gait if cmd > 0
cmd = torch.linalg.norm(env.command_manager.get_command(self.command_name), dim=1)
body_vel = torch.linalg.norm(self.asset.data.root_com_lin_vel_b[:, :2], dim=1)
reward = torch.where(
torch.logical_or(cmd > self.command_threshold, body_vel > self.velocity_threshold),
sync_reward * async_reward,
0.0,
)
# reward *= torch.clamp(-env.scene["robot"].data.projected_gravity_b[:, 2], 0, 0.7) / 0.7
return reward
"""
Helper functions.
"""
def _sync_reward_func(self, foot_0: int, foot_1: int) -> torch.Tensor:
"""Reward synchronization of two feet."""
air_time = self.contact_sensor.data.current_air_time
contact_time = self.contact_sensor.data.current_contact_time
# penalize the difference between the most recent air time and contact time of synced feet pairs.
se_air = torch.clip(torch.square(air_time[:, foot_0] - air_time[:, foot_1]), max=self.max_err**2)
se_contact = torch.clip(torch.square(contact_time[:, foot_0] - contact_time[:, foot_1]), max=self.max_err**2)
return torch.exp(-(se_air + se_contact) / self.std)
def _async_reward_func(self, foot_0: int, foot_1: int) -> torch.Tensor:
"""Reward anti-synchronization of two feet."""
air_time = self.contact_sensor.data.current_air_time
contact_time = self.contact_sensor.data.current_contact_time
# penalize the difference between opposing contact modes air time of feet 1 to contact time of feet 2
# and contact time of feet 1 to air time of feet 2) of feet pairs that are not in sync with each other.
se_act_0 = torch.clip(torch.square(air_time[:, foot_0] - contact_time[:, foot_1]), max=self.max_err**2)
se_act_1 = torch.clip(torch.square(contact_time[:, foot_0] - air_time[:, foot_1]), max=self.max_err**2)
return torch.exp(-(se_act_0 + se_act_1) / self.std)
def joint_mirror(env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg, mirror_joints: list[list[str]]) -> torch.Tensor:
# extract the used quantities (to enable type-hinting)
asset: Articulation = env.scene[asset_cfg.name]
if not hasattr(env, "joint_mirror_joints_cache") or env.joint_mirror_joints_cache is None:
# Cache joint positions for all pairs
env.joint_mirror_joints_cache = [
[asset.find_joints(joint_name) for joint_name in joint_pair] for joint_pair in mirror_joints
]
reward = torch.zeros(env.num_envs, device=env.device)
# Iterate over all joint pairs
for joint_pair in env.joint_mirror_joints_cache:
# Calculate the difference for each pair and add to the total reward
diff = torch.sum(
torch.square(asset.data.joint_pos[:, joint_pair[0][0]] - asset.data.joint_pos[:, joint_pair[1][0]]),
dim=-1,
)
reward += diff
reward *= 1 / len(mirror_joints) if len(mirror_joints) > 0 else 0
reward *= torch.clamp(-env.scene["robot"].data.projected_gravity_b[:, 2], 0, 0.7) / 0.7
return reward
def action_mirror(env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg, mirror_joints: list[list[str]]) -> torch.Tensor:
# extract the used quantities (to enable type-hinting)
asset: Articulation = env.scene[asset_cfg.name]
if not hasattr(env, "action_mirror_joints_cache") or env.action_mirror_joints_cache is None:
# Cache joint positions for all pairs
env.action_mirror_joints_cache = [
[asset.find_joints(joint_name) for joint_name in joint_pair] for joint_pair in mirror_joints
]
reward = torch.zeros(env.num_envs, device=env.device)
# Iterate over all joint pairs
for joint_pair in env.action_mirror_joints_cache:
# Calculate the difference for each pair and add to the total reward
diff = torch.sum(
torch.square(
torch.abs(env.action_manager.action[:, joint_pair[0][0]])
- torch.abs(env.action_manager.action[:, joint_pair[1][0]])
),
dim=-1,
)
reward += diff
reward *= 1 / len(mirror_joints) if len(mirror_joints) > 0 else 0
# reward *= torch.clamp(-env.scene["robot"].data.projected_gravity_b[:, 2], 0, 0.7) / 0.7
return reward
def action_sync(env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg, joint_groups: list[list[str]]) -> torch.Tensor:
# extract the used quantities (to enable type-hinting)
asset: Articulation = env.scene[asset_cfg.name]
# Cache joint indices if not already done
if not hasattr(env, "action_sync_joint_cache") or env.action_sync_joint_cache is None:
env.action_sync_joint_cache = [
[asset.find_joints(joint_name) for joint_name in joint_group] for joint_group in joint_groups
]
reward = torch.zeros(env.num_envs, device=env.device)
# Iterate over each joint group
for joint_group in env.action_sync_joint_cache:
if len(joint_group) < 2:
continue # need at least 2 joints to compare
# Get absolute actions for all joints in this group
actions = torch.stack(
[torch.abs(env.action_manager.action[:, joint[0]]) for joint in joint_group], dim=1
) # shape: (num_envs, num_joints_in_group)
# Calculate mean action for each environment
mean_actions = torch.mean(actions, dim=1, keepdim=True)
# Calculate variance from mean for each joint
variance = torch.mean(torch.square(actions - mean_actions), dim=1)
# Add to reward (we want to minimize this variance)
reward += variance.squeeze()
reward *= 1 / len(joint_groups) if len(joint_groups) > 0 else 0
# reward *= torch.clamp(-env.scene["robot"].data.projected_gravity_b[:, 2], 0, 0.7) / 0.7
return reward
# def feet_air_time(
# env: ManagerBasedRLEnv, command_name: str, sensor_cfg: SceneEntityCfg, threshold: float
# ) -> torch.Tensor:
# """Reward long steps taken by the feet using L2-kernel.
# This function rewards the agent for taking steps that are longer than a threshold. This helps ensure
# that the robot lifts its feet off the ground and takes steps. The reward is computed as the sum of
# the time for which the feet are in the air.
# If the commands are small (i.e. the agent is not supposed to take a step), then the reward is zero.
# """
# # extract the used quantities (to enable type-hinting)
# contact_sensor: ContactSensor = env.scene.sensors[sensor_cfg.name]
# # compute the reward
# first_contact = contact_sensor.compute_first_contact(env.step_dt)[:, sensor_cfg.body_ids]
# last_air_time = contact_sensor.data.last_air_time[:, sensor_cfg.body_ids]
# reward = torch.sum((last_air_time - threshold) * first_contact, dim=1)
# # no reward for zero command
# reward *= torch.norm(env.command_manager.get_command(command_name)[:, :2], dim=1) > 0.1
# # print(torch.norm(env.command_manager.get_command(command_name)[:, :2], dim=1), "command norm")
# reward *= torch.clamp(-env.scene["robot"].data.projected_gravity_b[:, 2], 0, 0.7) / 0.7
# return reward
# def feet_air_time(
# env: ManagerBasedRLEnv,
# asset_cfg: SceneEntityCfg,
# sensor_cfg: SceneEntityCfg,
# mode_time: float,
# velocity_threshold: float,
# ) -> torch.Tensor:
# """Reward longer feet air and contact time."""
# # extract the used quantities (to enable type-hinting)
# contact_sensor: ContactSensor = env.scene.sensors[sensor_cfg.name]
# asset: Articulation = env.scene[asset_cfg.name]
# if contact_sensor.cfg.track_air_time is False:
# raise RuntimeError("Activate ContactSensor's track_air_time!")
# # compute the reward
# current_air_time = contact_sensor.data.current_air_time[:, sensor_cfg.body_ids]
# current_contact_time = contact_sensor.data.current_contact_time[:, sensor_cfg.body_ids]
# t_max = torch.max(current_air_time, current_contact_time)
# t_min = torch.clip(t_max, max=mode_time)
# stance_cmd_reward = torch.clip(current_contact_time - current_air_time, -mode_time, mode_time)
# cmd = torch.norm(env.command_manager.get_command("base_velocity"), dim=1).unsqueeze(dim=1).expand(-1, 4)
# body_vel = torch.linalg.norm(asset.data.root_lin_vel_b[:, :2], dim=1).unsqueeze(dim=1).expand(-1, 4)
# reward = torch.where(
# torch.logical_or(cmd > 0.0, body_vel > velocity_threshold),
# torch.where(t_max < mode_time, t_min, 0),
# stance_cmd_reward,
# )
# return torch.sum(reward, dim=1)
def feet_air_time_positive_biped(env, command_name: str, threshold: float, sensor_cfg: SceneEntityCfg) -> torch.Tensor:
"""Reward long steps taken by the feet for bipeds.
This function rewards the agent for taking steps up to a specified threshold and also keep one foot at
a time in the air.
If the commands are small (i.e. the agent is not supposed to take a step), then the reward is zero.
"""
contact_sensor: ContactSensor = env.scene.sensors[sensor_cfg.name]
# compute the reward
air_time = contact_sensor.data.current_air_time[:, sensor_cfg.body_ids]
contact_time = contact_sensor.data.current_contact_time[:, sensor_cfg.body_ids]
in_contact = contact_time > 0.0
in_mode_time = torch.where(in_contact, contact_time, air_time)
single_stance = torch.sum(in_contact.int(), dim=1) == 1
reward = torch.min(torch.where(single_stance.unsqueeze(-1), in_mode_time, 0.0), dim=1)[0]
reward = torch.clamp(reward, max=threshold)
# no reward for zero command
reward *= torch.norm(env.command_manager.get_command(command_name)[:, :2], dim=1) > 0.1
# reward *= torch.clamp(-env.scene["robot"].data.projected_gravity_b[:, 2], 0, 0.7) / 0.7
return reward
def feet_air_time_variance_penalty(env: ManagerBasedRLEnv, sensor_cfg: SceneEntityCfg) -> torch.Tensor:
"""Penalize variance in the amount of time each foot spends in the air/on the ground relative to each other"""
# extract the used quantities (to enable type-hinting)
contact_sensor: ContactSensor = env.scene.sensors[sensor_cfg.name]
# compute the reward
last_air_time = contact_sensor.data.last_air_time[:, sensor_cfg.body_ids]
last_contact_time = contact_sensor.data.last_contact_time[:, sensor_cfg.body_ids]
reward = torch.var(torch.clip(last_air_time, max=0.5), dim=1) + torch.var(
torch.clip(last_contact_time, max=0.5), dim=1)
# print(last_air_time, "last air time")
# print(last_contact_time, "last contact time")
# reward *= torch.clamp(-env.scene["robot"].data.projected_gravity_b[:, 2], 0, 0.7) / 0.7
return reward
def feet_contact(
env: ManagerBasedRLEnv, command_name: str, expect_contact_num: int, sensor_cfg: SceneEntityCfg
) -> torch.Tensor:
"""Reward feet contact"""
# extract the used quantities (to enable type-hinting)
contact_sensor: ContactSensor = env.scene.sensors[sensor_cfg.name]
# compute the reward
contact = contact_sensor.compute_first_contact(env.step_dt)[:, sensor_cfg.body_ids]
contact_num = torch.sum(contact, dim=1)
reward = (contact_num != expect_contact_num).float()
# no reward for zero command
reward *= torch.linalg.norm(env.command_manager.get_command(command_name), dim=1) > 0.5
reward *= torch.clamp(-env.scene["robot"].data.projected_gravity_b[:, 2], 0, 0.7) / 0.7
return reward
def feet_contact_without_cmd(env: ManagerBasedRLEnv, command_name: str, sensor_cfg: SceneEntityCfg) -> torch.Tensor:
"""Reward feet contact"""
# extract the used quantities (to enable type-hinting)
contact_sensor: ContactSensor = env.scene.sensors[sensor_cfg.name]
# compute the reward
contact = contact_sensor.compute_first_contact(env.step_dt)[:, sensor_cfg.body_ids]
# print(contact, "contact")
reward = torch.sum(contact, dim=-1).float()
# print(reward, "reward after sum")
reward *= torch.linalg.norm(env.command_manager.get_command(command_name), dim=1) < 0.5
# print(env.command_manager.get_command(command_name), "env.command_manager.get_command(command_name)")
# print(reward, "reward after multiply")
# reward *= torch.clamp(-env.scene["robot"].data.projected_gravity_b[:, 2], 0, 0.7) / 0.7
return reward
def feet_stumble(env: ManagerBasedRLEnv, sensor_cfg: SceneEntityCfg) -> torch.Tensor:
# extract the used quantities (to enable type-hinting)
contact_sensor: ContactSensor = env.scene.sensors[sensor_cfg.name]
forces_z = torch.abs(contact_sensor.data.net_forces_w[:, sensor_cfg.body_ids, 2])
forces_xy = torch.linalg.norm(contact_sensor.data.net_forces_w[:, sensor_cfg.body_ids, :2], dim=2)
# Penalize feet hitting vertical surfaces
reward = torch.any(forces_xy > 4 * forces_z, dim=1).float()
# reward *= torch.clamp(-env.scene["robot"].data.projected_gravity_b[:, 2], 0, 0.7) / 0.7
return reward
def feet_distance_y_exp(
env: ManagerBasedRLEnv, stance_width: float, std: float, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot")
) -> torch.Tensor:
asset: RigidObject = env.scene[asset_cfg.name]
cur_footsteps_translated = asset.data.body_link_pos_w[:, asset_cfg.body_ids, :] - asset.data.root_link_pos_w[
:, :
].unsqueeze(1)
n_feet = len(asset_cfg.body_ids)
footsteps_in_body_frame = torch.zeros(env.num_envs, n_feet, 3, device=env.device)
for i in range(n_feet):
footsteps_in_body_frame[:, i, :] = math_utils.quat_apply(
math_utils.quat_conjugate(asset.data.root_link_quat_w), cur_footsteps_translated[:, i, :]
)
side_sign = torch.tensor(
[1.0 if i % 2 == 0 else -1.0 for i in range(n_feet)],
device=env.device,
)
stance_width_tensor = stance_width * torch.ones([env.num_envs, 1], device=env.device)
desired_ys = stance_width_tensor / 2 * side_sign.unsqueeze(0)
stance_diff = torch.square(desired_ys - footsteps_in_body_frame[:, :, 1])
reward = torch.exp(-torch.sum(stance_diff, dim=1) / (std**2))
# reward *= torch.clamp(-env.scene["robot"].data.projected_gravity_b[:, 2], 0, 0.7) / 0.7
return reward
def feet_distance_xy_exp(
env: ManagerBasedRLEnv,
stance_width: float,
stance_length: float,
std: float,
asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"),
) -> torch.Tensor:
asset: RigidObject = env.scene[asset_cfg.name]
# Compute the current footstep positions relative to the root
cur_footsteps_translated = asset.data.body_link_pos_w[:, asset_cfg.body_ids, :] - asset.data.root_link_pos_w[
:, :
].unsqueeze(1)
footsteps_in_body_frame = torch.zeros(env.num_envs, 4, 3, device=env.device)
for i in range(4):
footsteps_in_body_frame[:, i, :] = math_utils.quat_apply(
math_utils.quat_conjugate(asset.data.root_link_quat_w), cur_footsteps_translated[:, i, :]
)
# Desired x and y positions for each foot
stance_width_tensor = stance_width * torch.ones([env.num_envs, 1], device=env.device)
stance_length_tensor = stance_length * torch.ones([env.num_envs, 1], device=env.device)
desired_xs = torch.cat(
[stance_length_tensor / 2, stance_length_tensor / 2, -stance_length_tensor / 2, -stance_length_tensor / 2],
dim=1,
)
desired_ys = torch.cat(
[stance_width_tensor / 2, -stance_width_tensor / 2, stance_width_tensor / 2, -stance_width_tensor / 2], dim=1
)
# Compute differences in x and y
stance_diff_x = torch.square(desired_xs - footsteps_in_body_frame[:, :, 0])
stance_diff_y = torch.square(desired_ys - footsteps_in_body_frame[:, :, 1])
# Combine x and y differences and compute the exponential penalty
stance_diff = stance_diff_x + stance_diff_y
reward = torch.exp(-torch.sum(stance_diff, dim=1) / std**2)
# reward *= torch.clamp(-env.scene["robot"].data.projected_gravity_b[:, 2], 0, 0.7) / 0.7
return reward
def feet_height(
env: ManagerBasedRLEnv,
command_name: str,
asset_cfg: SceneEntityCfg,
target_height: float,
tanh_mult: float,
) -> torch.Tensor:
"""Reward the swinging feet for clearing a specified height off the ground"""
asset: RigidObject = env.scene[asset_cfg.name]
foot_z_target_error = torch.square(asset.data.body_pos_w[:, asset_cfg.body_ids, 2] - target_height)
# foot_velocity_tanh = torch.tanh(
# tanh_mult * torch.linalg.norm(asset.data.body_lin_vel_w[:, asset_cfg.body_ids, :2], dim=2)
# )
# reward = torch.sum(foot_z_target_error * foot_velocity_tanh, dim=1)
reward = torch.sum(foot_z_target_error, dim=1)
# print(foot_z_target_error, "foot_z_target_error")
# no reward for zero command
reward *= torch.linalg.norm(env.command_manager.get_command(command_name), dim=1) > 0.2
# reward *= torch.clamp(-env.scene["robot"].data.projected_gravity_b[:, 2], 0, 0.7) / 0.7
return reward
def feet_height_body(
env: ManagerBasedRLEnv,
command_name: str,
asset_cfg: SceneEntityCfg,
target_height: float,
tanh_mult: float,
) -> torch.Tensor:
"""Reward the swinging feet for clearing a specified height off the ground"""
asset: RigidObject = env.scene[asset_cfg.name]
cur_footpos_translated = asset.data.body_pos_w[:, asset_cfg.body_ids, :] - asset.data.root_pos_w[:, :].unsqueeze(1)
footpos_in_body_frame = torch.zeros(env.num_envs, len(asset_cfg.body_ids), 3, device=env.device)
cur_footvel_translated = asset.data.body_lin_vel_w[:, asset_cfg.body_ids, :] - asset.data.root_lin_vel_w[
:, :
].unsqueeze(1)
footvel_in_body_frame = torch.zeros(env.num_envs, len(asset_cfg.body_ids), 3, device=env.device)
for i in range(len(asset_cfg.body_ids)):
footpos_in_body_frame[:, i, :] = math_utils.quat_apply_inverse(
asset.data.root_quat_w, cur_footpos_translated[:, i, :]
)
footvel_in_body_frame[:, i, :] = math_utils.quat_apply_inverse(
asset.data.root_quat_w, cur_footvel_translated[:, i, :]
)
foot_z_target_error = torch.square(footpos_in_body_frame[:, :, 2] - target_height).view(env.num_envs, -1)
foot_velocity_tanh = torch.tanh(tanh_mult * torch.norm(footvel_in_body_frame[:, :, :2], dim=2))
reward = torch.sum(foot_z_target_error * foot_velocity_tanh, dim=1)
reward *= torch.linalg.norm(env.command_manager.get_command(command_name), dim=1) > 0.1
# reward *= torch.clamp(-env.scene["robot"].data.projected_gravity_b[:, 2], 0, 0.7) / 0.7
return reward
def feet_slide(
env: ManagerBasedRLEnv, sensor_cfg: SceneEntityCfg, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot")
) -> torch.Tensor:
"""Penalize feet sliding.
This function penalizes the agent for sliding its feet on the ground. The reward is computed as the
norm of the linear velocity of the feet multiplied by a binary contact sensor. This ensures that the
agent is penalized only when the feet are in contact with the ground.
"""
# Penalize feet sliding
contact_sensor: ContactSensor = env.scene.sensors[sensor_cfg.name]
contacts = contact_sensor.data.net_forces_w_history[:, :, sensor_cfg.body_ids, :].norm(dim=-1).max(dim=1)[0] > 1.0
asset: RigidObject = env.scene[asset_cfg.name]
# feet_vel = asset.data.body_lin_vel_w[:, asset_cfg.body_ids, :2]
# reward = torch.sum(feet_vel.norm(dim=-1) * contacts, dim=1)
cur_footvel_translated = asset.data.body_lin_vel_w[:, asset_cfg.body_ids, :] - asset.data.root_lin_vel_w[
:, :
].unsqueeze(1)
footvel_in_body_frame = torch.zeros(env.num_envs, len(asset_cfg.body_ids), 3, device=env.device)
for i in range(len(asset_cfg.body_ids)):
footvel_in_body_frame[:, i, :] = math_utils.quat_apply_inverse(
asset.data.root_quat_w, cur_footvel_translated[:, i, :]
)
foot_leteral_vel = torch.sqrt(torch.sum(torch.square(footvel_in_body_frame[:, :, :2]), dim=2)).view(
env.num_envs, -1
)
reward = torch.sum(foot_leteral_vel * contacts, dim=1)
# reward *= torch.clamp(-env.scene["robot"].data.projected_gravity_b[:, 2], 0, 0.7) / 0.7
return reward
# def stand_still_joint_deviation_l1(
# env, command_name: str, command_threshold: float = 0.06, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot")
# ) -> torch.Tensor:
# """Penalize offsets from the default joint positions when the command is very small."""
# command = env.command_manager.get_command(command_name)
# # Penalize motion when command is nearly zero.
# return joint_deviation_l1(env, asset_cfg) * (torch.norm(command[:, :], dim=1) < command_threshold)
# def joint_deviation_l1(env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot")) -> torch.Tensor:
# """Penalize joint positions that deviate from the default one."""
# # extract the used quantities (to enable type-hinting)
# asset: Articulation = env.scene[asset_cfg.name]
# # compute out of limits constraints
# angle = asset.data.joint_pos[:, asset_cfg.joint_ids] - asset.data.default_joint_pos[:, asset_cfg.joint_ids]
# return torch.sum(torch.abs(angle), dim=1)
# def smoothness_1(env: ManagerBasedRLEnv) -> torch.Tensor:
# # Penalize changes in actions
# diff = torch.square(env.action_manager.action - env.action_manager.prev_action)
# diff = diff * (env.action_manager.prev_action[:, :] != 0) # ignore first step
# return torch.sum(diff, dim=1)
# def joint_acc_l2_new(env: ManagerBasedRLEnv) -> torch.Tensor:
# def smoothness_2(env: ManagerBasedRLEnv) -> torch.Tensor:
# # Penalize changes in actions
# diff = torch.square(env.action_manager.action - 2 * env.action_manager.prev_action + env.action_manager.prev_prev_action)
# diff = diff * (env.action_manager.prev_action[:, :] != 0) # ignore first step
# diff = diff * (env.action_manager.prev_prev_action[:, :] != 0) # ignore second step
# # print(torch.sum(diff, dim=1), "smoothness l2")
# return torch.sum(diff, dim=1)
def upward(env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot")) -> torch.Tensor:
"""Penalize z-axis base linear velocity using L2 squared kernel."""
# extract the used quantities (to enable type-hinting)
asset: RigidObject = env.scene[asset_cfg.name]
reward = torch.square(1 - asset.data.projected_gravity_b[:, 2])
return reward
def base_height_l2(
env: ManagerBasedRLEnv,
target_height: float,
asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"),
sensor_cfg: SceneEntityCfg | None = None,
) -> torch.Tensor:
"""Penalize asset height from its target using L2 squared kernel.
Note:
For flat terrain, target height is in the world frame. For rough terrain,
sensor readings can adjust the target height to account for the terrain.
"""
# extract the used quantities (to enable type-hinting)
asset: RigidObject = env.scene[asset_cfg.name]
if sensor_cfg is not None:
sensor: RayCaster = env.scene[sensor_cfg.name]
# Adjust the target height using the sensor data
ray_hits = sensor.data.ray_hits_w[..., 2]
if torch.isnan(ray_hits).any() or torch.isinf(ray_hits).any() or torch.max(torch.abs(ray_hits)) > 1e6:
adjusted_target_height = asset.data.root_link_pos_w[:, 2]
else:
adjusted_target_height = target_height + torch.mean(ray_hits, dim=1)
else:
# Use the provided target height directly for flat terrain
adjusted_target_height = target_height
# Compute the L2 squared penalty
reward = torch.square(asset.data.root_pos_w[:, 2] - adjusted_target_height)
# reward *= torch.clamp(-env.scene["robot"].data.projected_gravity_b[:, 2], 0, 0.7) / 0.7
return reward
def lin_vel_z_l2(env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot")) -> torch.Tensor:
"""Penalize z-axis base linear velocity using L2 squared kernel."""
# extract the used quantities (to enable type-hinting)
asset: RigidObject = env.scene[asset_cfg.name]
reward = torch.square(asset.data.root_lin_vel_b[:, 2])
# reward *= torch.clamp(-env.scene["robot"].data.projected_gravity_b[:, 2], 0, 0.7) / 0.7
return reward
def ang_vel_xy_l2(env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot")) -> torch.Tensor:
"""Penalize xy-axis base angular velocity using L2 squared kernel."""
# extract the used quantities (to enable type-hinting)
asset: RigidObject = env.scene[asset_cfg.name]
reward = torch.sum(torch.square(asset.data.root_ang_vel_b[:, :2]), dim=1)
# reward *= torch.clamp(-env.scene["robot"].data.projected_gravity_b[:, 2], 0, 0.7) / 0.7
return reward
def undesired_contacts(env: ManagerBasedRLEnv, threshold: float, sensor_cfg: SceneEntityCfg) -> torch.Tensor:
"""Penalize undesired contacts as the number of violations that are above a threshold."""
# extract the used quantities (to enable type-hinting)
contact_sensor: ContactSensor = env.scene.sensors[sensor_cfg.name]
# check if contact force is above threshold
net_contact_forces = contact_sensor.data.net_forces_w_history
is_contact = torch.max(torch.norm(net_contact_forces[:, :, sensor_cfg.body_ids], dim=-1), dim=1)[0] > threshold
# sum over contacts for each environment
reward = torch.sum(is_contact, dim=1).float()
# reward *= torch.clamp(-env.scene["robot"].data.projected_gravity_b[:, 2], 0, 0.7) / 0.7
return reward
def flat_orientation_l2(env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot")) -> torch.Tensor:
"""Penalize non-flat base orientation using L2 squared kernel.
This is computed by penalizing the xy-components of the projected gravity vector.
"""
# extract the used quantities (to enable type-hinting)
asset: RigidObject = env.scene[asset_cfg.name]
reward = torch.sum(torch.square(asset.data.projected_gravity_b[:, :2]), dim=1)
# reward *= torch.clamp(-env.scene["robot"].data.projected_gravity_b[:, 2], 0, 0.7) / 0.7
return reward
def feet_air_time_including_ang_z(
env: ManagerBasedRLEnv, command_name: str, sensor_cfg: SceneEntityCfg, threshold: float
) -> torch.Tensor:
"""Reward long steps taken by the feet using L2-kernel.
This function rewards the agent for taking steps that are longer than a threshold. This helps ensure
that the robot lifts its feet off the ground and takes steps. The reward is computed as the sum of
the time for which the feet are in the air.
If the commands are small (i.e. the agent is not supposed to take a step), then the reward is zero.
"""
# extract the used quantities (to enable type-hinting)
contact_sensor: ContactSensor = env.scene.sensors[sensor_cfg.name]
# compute the reward
first_contact = contact_sensor.compute_first_contact(env.step_dt)[:, sensor_cfg.body_ids]
last_air_time = contact_sensor.data.last_air_time[:, sensor_cfg.body_ids]
reward = torch.sum((last_air_time - threshold) * first_contact, dim=1)
# no reward for zero command
reward *= torch.norm(env.command_manager.get_command(command_name), dim=1) > 0.1
# reward *= torch.norm(env.command_manager.get_command(command_name)[:, :3], dim=1) > 0.1
return reward
def lin_vel_xy_l2_with_ang_z_command(
env: ManagerBasedRLEnv,
command_name: str,
command_threshold: float,
asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"),
) -> torch.Tensor:
"""Penalize xy-axis base linear velocity using L2 squared kernel if command is ang_vel_z."""
# extract the used quantities (to enable type-hinting)
asset: RigidObject = env.scene[asset_cfg.name]
# reward = torch.square(asset.data.root_lin_vel_b[:, 2])
reward = torch.sum(torch.square(asset.data.root_lin_vel_b[:, :2]), dim=1)
command = env.command_manager.get_command(command_name)
reward *= (torch.sum(torch.square(command[:, 2:]), dim=1) > command_threshold) & \
(torch.sum(torch.square(command[:, :2]), dim=1) < command_threshold)
# reward *= torch.sum(torch.square(env.command_manager.get_command(command_name)[:, 2:]), dim=1) > command_threshold
# reward *= torch.clamp(-env.scene["robot"].data.projected_gravity_b[:, 2], 0, 0.7) / 0.7
return reward
@@ -0,0 +1,866 @@
# Copyright (c) 2025 Deep Robotics
# SPDX-License-Identifier: BSD 3-Clause
# Copyright (c) 2024-2025 Ziqi Fan
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) 2022-2025, The Isaac Lab Project Developers.
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
import inspect
import math
import sys
from dataclasses import MISSING
import isaaclab.sim as sim_utils
from isaaclab.assets import ArticulationCfg, AssetBaseCfg
from isaaclab.envs import ManagerBasedRLEnvCfg
from isaaclab.managers import CurriculumTermCfg as CurrTerm
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.sensors import ContactSensorCfg, RayCasterCfg, patterns
from isaaclab.terrains import TerrainImporterCfg
from isaaclab.utils import configclass
from isaaclab.utils.noise import AdditiveUniformNoiseCfg as Unoise
import rl_training.tasks.manager_based.locomotion.velocity.mdp as mdp
##
# Pre-defined configs
##
from isaaclab.terrains.config.rough import ROUGH_TERRAINS_CFG # isort: skip
##
# Scene definition
##
@configclass
class MySceneCfg(InteractiveSceneCfg):
"""Configuration for the terrain scene with a legged robot."""
# ground terrain
terrain = TerrainImporterCfg(
prim_path="/World/ground",
terrain_type="generator",
terrain_generator=ROUGH_TERRAINS_CFG,
max_init_terrain_level=5,
collision_group=-1,
physics_material=sim_utils.RigidBodyMaterialCfg(
friction_combine_mode="multiply",
restitution_combine_mode="multiply",
static_friction=1.0,
dynamic_friction=1.0,
restitution=1.0,
),
# Keep scene startup independent of NVIDIA's online asset server.
# The robot visual geometry is still loaded from the STL meshes in its URDF.
visual_material=sim_utils.PreviewSurfaceCfg(
diffuse_color=(0.18, 0.20, 0.22),
roughness=0.9,
),
debug_vis=False,
)
# robots
robot: ArticulationCfg = MISSING
# sensors
height_scanner = RayCasterCfg(
prim_path="{ENV_REGEX_NS}/Robot/base",
offset=RayCasterCfg.OffsetCfg(pos=(0.0, 0.0, 20.0)),
ray_alignment="yaw",
pattern_cfg=patterns.GridPatternCfg(resolution=0.1, size=[1.6, 1.0]),
debug_vis=False,
mesh_prim_paths=["/World/ground"],
)
height_scanner_base = RayCasterCfg(
prim_path="{ENV_REGEX_NS}/Robot/base",
offset=RayCasterCfg.OffsetCfg(pos=(0.0, 0.0, 20.0)),
ray_alignment="yaw",
pattern_cfg=patterns.GridPatternCfg(resolution=0.05, size=(0.1, 0.1)),
debug_vis=False,
mesh_prim_paths=["/World/ground"],
)
contact_forces = ContactSensorCfg(prim_path="{ENV_REGEX_NS}/Robot/.*", history_length=3, track_air_time=True)
# lights
sky_light = AssetBaseCfg(
prim_path="/World/skyLight",
spawn=sim_utils.DomeLightCfg(
intensity=1200.0,
color=(0.75, 0.75, 0.75),
),
)
##
# MDP settings
##
@configclass
class CommandsCfg:
"""Command specifications for the MDP."""
base_velocity = mdp.UniformThresholdVelocityCommandCfg(
asset_name="robot",
resampling_time_range=(10.0, 10.0),
rel_standing_envs=0.02,
rel_heading_envs=1.0,
heading_command=True,
heading_control_stiffness=0.5,
debug_vis=True,
ranges=mdp.UniformThresholdVelocityCommandCfg.Ranges(
lin_vel_x=(-1.0, 1.0), lin_vel_y=(-1.0, 1.0), ang_vel_z=(-1.0, 1.0), heading=(-math.pi, math.pi)
),
)
@configclass
class ActionsCfg:
"""Action specifications for the MDP."""
joint_pos = mdp.JointPositionActionCfg(
asset_name="robot", joint_names=[".*"], scale=0.5, use_default_offset=True, clip=None, preserve_order=True
)
@configclass
class ObservationsCfg:
"""Observation specifications for the MDP."""
@configclass
class PolicyCfg(ObsGroup):
"""Observations for policy group."""
# observation terms (order preserved)
base_lin_vel = ObsTerm(
func=mdp.base_lin_vel,
noise=Unoise(n_min=-0.1, n_max=0.1),
clip=(-100.0, 100.0),
scale=1.0,
)
base_ang_vel = ObsTerm(
func=mdp.base_ang_vel,
noise=Unoise(n_min=-0.2, n_max=0.2),
clip=(-100.0, 100.0),
scale=1.0,
)
projected_gravity = ObsTerm(
func=mdp.projected_gravity,
noise=Unoise(n_min=-0.05, n_max=0.05),
clip=(-100.0, 100.0),
scale=1.0,
)
velocity_commands = ObsTerm(
func=mdp.generated_commands,
params={"command_name": "base_velocity"},
clip=(-100.0, 100.0),
scale=1.0,
)
joint_pos = ObsTerm(
func=mdp.joint_pos_rel,
params={"asset_cfg": SceneEntityCfg("robot", joint_names=".*", preserve_order=True)},
noise=Unoise(n_min=-0.01, n_max=0.01),
clip=(-100.0, 100.0),
scale=1.0,
)
joint_vel = ObsTerm(
func=mdp.joint_vel_rel,
params={"asset_cfg": SceneEntityCfg("robot", joint_names=".*", preserve_order=True)},
noise=Unoise(n_min=-1.5, n_max=1.5),
clip=(-100.0, 100.0),
scale=1.0,
)
actions = ObsTerm(
func=mdp.last_action,
clip=(-100.0, 100.0),
scale=1.0,
)
height_scan = ObsTerm(
func=mdp.height_scan,
params={"sensor_cfg": SceneEntityCfg("height_scanner")},
noise=Unoise(n_min=-0.1, n_max=0.1),
clip=(-1.0, 1.0),
scale=1.0,
)
def __post_init__(self):
self.enable_corruption = True
self.concatenate_terms = True
@configclass
class CriticCfg(ObsGroup):
"""Observations for critic group."""
# observation terms (order preserved)
base_lin_vel = ObsTerm(
func=mdp.base_lin_vel,
clip=(-100.0, 100.0),
scale=1.0,
)
base_ang_vel = ObsTerm(
func=mdp.base_ang_vel,
clip=(-100.0, 100.0),
scale=1.0,
)
projected_gravity = ObsTerm(
func=mdp.projected_gravity,
clip=(-100.0, 100.0),
scale=1.0,
)
velocity_commands = ObsTerm(
func=mdp.generated_commands,
params={"command_name": "base_velocity"},
clip=(-100.0, 100.0),
scale=1.0,
)
joint_pos = ObsTerm(
func=mdp.joint_pos_rel,
params={"asset_cfg": SceneEntityCfg("robot", joint_names=".*", preserve_order=True)},
clip=(-100.0, 100.0),
scale=1.0,
)
joint_vel = ObsTerm(
func=mdp.joint_vel_rel,
params={"asset_cfg": SceneEntityCfg("robot", joint_names=".*", preserve_order=True)},
clip=(-100.0, 100.0),
scale=1.0,
)
actions = ObsTerm(
func=mdp.last_action,
clip=(-100.0, 100.0),
scale=1.0,
)
height_scan = ObsTerm(
func=mdp.height_scan,
params={"sensor_cfg": SceneEntityCfg("height_scanner")},
clip=(-1.0, 1.0),
scale=1.0,
)
# joint_effort = ObsTerm(
# func=mdp.joint_effort,
# clip=(-100, 100),
# scale=0.01,
# )
def __post_init__(self):
self.enable_corruption = False
self.concatenate_terms = True
# observation groups
policy: PolicyCfg = PolicyCfg()
critic: CriticCfg = CriticCfg()
@configclass
class EventCfg:
"""Configuration for events."""
# startup
randomize_rigid_body_material = EventTerm(
func=mdp.randomize_rigid_body_material,
mode="startup",
params={
"asset_cfg": SceneEntityCfg("robot", body_names=".*"),
"static_friction_range": (0.3, 1.0),
"dynamic_friction_range": (0.3, 0.8),
"restitution_range": (0.0, 0.4),
"num_buckets": 1024,
},
)
randomize_rigid_body_mass = EventTerm(
func=mdp.randomize_rigid_body_mass,
mode="startup",
params={
"asset_cfg": SceneEntityCfg("robot", body_names=""),
"mass_distribution_params": (0.85, 1.15),
"operation": "scale",
"recompute_inertia": True,
},
)
randomize_rigid_body_mass_base = EventTerm(
func=mdp.randomize_rigid_body_mass,
mode="startup",
params={
"asset_cfg": SceneEntityCfg("robot", body_names=""),
"mass_distribution_params": (-1.0, 3.0),
"operation": "add",
"recompute_inertia": True,
},
)
randomize_rigid_body_inertia = EventTerm(
func=mdp.randomize_rigid_body_inertia,
mode="startup",
params={
"asset_cfg": SceneEntityCfg("robot", body_names=".*"),
"inertia_distribution_params": (0.85, 1.15),
"operation": "scale",
},
)
randomize_com_positions = EventTerm(
func=mdp.randomize_rigid_body_com,
mode="startup",
params={
"asset_cfg": SceneEntityCfg("robot", body_names=".*"),
"com_range": {"x": (-0.03, 0.03), "y": (-0.03, 0.03), "z": (-0.02, 0.02)},
},
)
# reset
randomize_apply_external_force_torque = EventTerm(
func=mdp.apply_external_force_torque,
mode="reset",
params={
"asset_cfg": SceneEntityCfg("robot", body_names=""),
"force_range": (-10.0, 10.0),
"torque_range": (-10.0, 10.0),
},
)
randomize_reset_joints = EventTerm(
func=mdp.reset_joints_by_scale,
# func=mdp.reset_joints_by_offset,
mode="reset",
params={
"position_range": (1.0, 1.0),
"velocity_range": (0.0, 0.0),
},
)
randomize_actuator_gains = EventTerm(
func=mdp.randomize_actuator_gains,
mode="reset",
params={
"asset_cfg": SceneEntityCfg("robot", joint_names=".*"),
"stiffness_distribution_params": (0.85, 1.15),
"damping_distribution_params": (0.85, 1.15),
"operation": "scale",
"distribution": "uniform",
},
)
randomize_reset_base = EventTerm(
func=mdp.reset_root_state_uniform,
mode="reset",
params={
"pose_range": {"x": (-0.5, 0.5), "y": (-0.5, 0.5), "yaw": (-3.14, 3.14)},
"velocity_range": {
"x": (-0.5, 0.5),
"y": (-0.5, 0.5),
"z": (-0.5, 0.5),
"roll": (-0.5, 0.5),
"pitch": (-0.5, 0.5),
"yaw": (-0.5, 0.5),
},
},
)
# interval
randomize_push_robot = EventTerm(
func=mdp.push_by_setting_velocity,
mode="interval",
interval_range_s=(10.0, 15.0),
params={"velocity_range": {"x": (-0.5, 0.5), "y": (-0.5, 0.5)}},
)
@configclass
class RewardsCfg:
"""Reward terms for the MDP."""
# General
is_terminated = RewTerm(func=mdp.is_terminated, weight=0.0)
# Root penalties
lin_vel_z_l2 = RewTerm(func=mdp.lin_vel_z_l2, weight=0.0)
ang_vel_xy_l2 = RewTerm(func=mdp.ang_vel_xy_l2, weight=0.0)
flat_orientation_l2 = RewTerm(func=mdp.flat_orientation_l2, weight=0.0)
base_height_l2 = RewTerm(
func=mdp.base_height_l2,
weight=0.0,
params={
"asset_cfg": SceneEntityCfg("robot", body_names=""),
"sensor_cfg": SceneEntityCfg("height_scanner_base"),
"target_height": 0.0,
},
)
body_lin_acc_l2 = RewTerm(
func=mdp.body_lin_acc_l2,
weight=0.0,
params={"asset_cfg": SceneEntityCfg("robot", body_names="")},
)
# Joint penalties
joint_torques_l2 = RewTerm(
func=mdp.joint_torques_l2, weight=0.0, params={"asset_cfg": SceneEntityCfg("robot", joint_names=".*")}
)
joint_vel_l2 = RewTerm(
func=mdp.joint_vel_l2, weight=0.0, params={"asset_cfg": SceneEntityCfg("robot", joint_names=".*")}
)
joint_acc_l2 = RewTerm(
func=mdp.joint_acc_l2, weight=0.0, params={"asset_cfg": SceneEntityCfg("robot", joint_names=".*")}
)
joint_deviation_l1 = RewTerm(
func=mdp.joint_deviation_l1,
weight=0.0,
params={"asset_cfg": SceneEntityCfg("robot", joint_names=".*")},
)
def create_joint_deviation_l1_rewterm(self, attr_name, weight, joint_names_pattern):
rew_term = RewTerm(
func=mdp.joint_deviation_l1,
weight=weight,
params={"asset_cfg": SceneEntityCfg("robot", joint_names=joint_names_pattern)},
)
setattr(self, attr_name, rew_term)
joint_pos_limits = RewTerm(
func=mdp.joint_pos_limits, weight=0.0, params={"asset_cfg": SceneEntityCfg("robot", joint_names=".*")}
)
joint_vel_limits = RewTerm(
func=mdp.joint_vel_limits,
weight=0.0,
params={"asset_cfg": SceneEntityCfg("robot", joint_names=".*"), "soft_ratio": 1.0},
)
joint_power = RewTerm(
func=mdp.joint_power,
weight=0.0,
params={
"asset_cfg": SceneEntityCfg("robot", joint_names=".*"),
},
)
stand_still_without_cmd = RewTerm(
func=mdp.stand_still_without_cmd,
weight=0.0,
params={
"command_name": "base_velocity",
"command_threshold": 0.1,
"asset_cfg": SceneEntityCfg("robot", joint_names=".*"),
},
)
joint_pos_penalty = RewTerm(
func=mdp.joint_pos_penalty,
weight=0.0,
params={
"command_name": "base_velocity",
"asset_cfg": SceneEntityCfg("robot", joint_names=".*"),
"stand_still_scale": 5.0,
"velocity_threshold": 0.5,
"command_threshold": 0.1,
},
)
hipx_joint_pos_penalty = RewTerm(
func=mdp.joint_pos_penalty,
weight=0.0,
params={
"command_name": "base_velocity",
"asset_cfg": SceneEntityCfg("robot", joint_names=".*"),
"stand_still_scale": 5.0,
"velocity_threshold": 0.5,
"command_threshold": 0.1,
},
)
hipy_joint_pos_penalty = RewTerm(
func=mdp.joint_pos_penalty,
weight=0.0,
params={
"command_name": "base_velocity",
"asset_cfg": SceneEntityCfg("robot", joint_names=".*"),
"stand_still_scale": 5.0,
"velocity_threshold": 0.5,
"command_threshold": 0.1,
},
)
knee_joint_pos_penalty = RewTerm(
func=mdp.joint_pos_penalty,
weight=0.0,
params={
"command_name": "base_velocity",
"asset_cfg": SceneEntityCfg("robot", joint_names=".*"),
"stand_still_scale": 5.0,
"velocity_threshold": 0.5,
"command_threshold": 0.1,
},
)
wheel_vel_penalty = RewTerm(
func=mdp.wheel_vel_penalty,
weight=0.0,
params={
"asset_cfg": SceneEntityCfg("robot", joint_names=""),
"sensor_cfg": SceneEntityCfg("contact_forces", body_names=""),
"command_name": "base_velocity",
"velocity_threshold": 0.5,
"command_threshold": 0.1,
},
)
joint_mirror = RewTerm(
func=mdp.joint_mirror,
weight=0.0,
params={
"asset_cfg": SceneEntityCfg("robot"),
"mirror_joints": [["FR.*", "RL.*"], ["FL.*", "RR.*"]],
},
)
action_mirror = RewTerm(
func=mdp.action_mirror,
weight=0.0,
params={
"asset_cfg": SceneEntityCfg("robot"),
"mirror_joints": [["FR.*", "RL.*"], ["FL.*", "RR.*"]],
},
)
action_sync = RewTerm(
func=mdp.action_sync,
weight=0.0,
params={
"asset_cfg": SceneEntityCfg("robot"),
"joint_groups": [
["FR_hip_joint", "FL_hip_joint", "RL_hip_joint", "RR_hip_joint"],
["FR_thigh_joint", "FL_thigh_joint", "RL_thigh_joint", "RR_thigh_joint"],
["FR_calf_joint", "FL_calf_joint", "RL_calf_joint", "RR_calf_joint"],
],
},
)
# Action penalties
applied_torque_limits = RewTerm(
func=mdp.applied_torque_limits,
weight=0.0,
params={"asset_cfg": SceneEntityCfg("robot", joint_names=".*")},
)
action_rate_l2 = RewTerm(func=mdp.action_rate_l2, weight=0.0)
# smoothness_1 = RewTerm(func=mdp.smoothness_1, weight=0.0) # Same as action_rate_l2
# smoothness_2 = RewTerm(func=mdp.smoothness_2, weight=0.0) # Unvaliable now
# Contact sensor
undesired_contacts = RewTerm(
func=mdp.undesired_contacts,
weight=0.0,
params={
"sensor_cfg": SceneEntityCfg("contact_forces", body_names=""),
"threshold": 1.0,
},
)
contact_forces = RewTerm(
func=mdp.contact_forces,
weight=0.0,
params={"sensor_cfg": SceneEntityCfg("contact_forces", body_names=""), "threshold": 100.0},
)
# Velocity-tracking rewards
track_lin_vel_xy_exp = RewTerm(
func=mdp.track_lin_vel_xy_exp, weight=0.0, params={"command_name": "base_velocity", "std": math.sqrt(0.5)}
)
track_ang_vel_z_exp = RewTerm(
func=mdp.track_ang_vel_z_exp, weight=0.0, params={"command_name": "base_velocity", "std": math.sqrt(0.5)}
)
# Others
# feet_air_time = RewTerm(
# func=mdp.feet_air_time,
# weight=0.0,
# params={
# "command_name": "base_velocity",
# "threshold": 0.5,
# "sensor_cfg": SceneEntityCfg("contact_forces", body_names=""),
# },
# )
feet_air_time = RewTerm(
func=mdp.feet_air_time_including_ang_z,
weight=0.0,
params={
"command_name": "base_velocity",
"threshold": 0.5,
"sensor_cfg": SceneEntityCfg("contact_forces", body_names=""),
},
)
feet_air_time_variance = RewTerm(
func=mdp.feet_air_time_variance_penalty,
weight=0,
params={"sensor_cfg": SceneEntityCfg("contact_forces", body_names="")},
)
feet_gait = RewTerm(
func=mdp.GaitReward,
weight=0.0,
params={
"std": math.sqrt(0.5),
"command_name": "base_velocity",
"max_err": 0.2,
"velocity_threshold": 0.5,
"command_threshold": 0.1,
"synced_feet_pair_names": (("", ""), ("", "")),
"asset_cfg": SceneEntityCfg("robot"),
"sensor_cfg": SceneEntityCfg("contact_forces"),
},
)
feet_contact = RewTerm(
func=mdp.feet_contact,
weight=0.0,
params={
"sensor_cfg": SceneEntityCfg("contact_forces", body_names=""),
"command_name": "base_velocity",
"expect_contact_num": 2,
},
)
feet_contact_without_cmd = RewTerm(
func=mdp.feet_contact_without_cmd,
weight=0.0,
params={
"sensor_cfg": SceneEntityCfg("contact_forces", body_names=""),
"command_name": "base_velocity",
},
)
feet_stumble = RewTerm(
func=mdp.feet_stumble,
weight=0.0,
params={
"sensor_cfg": SceneEntityCfg("contact_forces", body_names=""),
},
)
feet_slide = RewTerm(
func=mdp.feet_slide,
weight=0.0,
params={
"sensor_cfg": SceneEntityCfg("contact_forces", body_names=""),
"asset_cfg": SceneEntityCfg("robot", body_names=""),
},
)
stand_still = RewTerm(
func=mdp.stand_still_joint_deviation_l1,
weight=0,
params={
"command_name": "base_velocity",
"asset_cfg": SceneEntityCfg("robot", joint_names=""),
},
) # negetive
feet_height = RewTerm(
func=mdp.feet_height,
weight=0.0,
params={
"asset_cfg": SceneEntityCfg("robot", body_names=""),
"tanh_mult": 2.0,
"target_height": 0.05,
"command_name": "base_velocity",
},
)
feet_height_body = RewTerm(
func=mdp.feet_height_body,
weight=0.0,
params={
"asset_cfg": SceneEntityCfg("robot", body_names=""),
"tanh_mult": 2.0,
"target_height": -0.3,
"command_name": "base_velocity",
},
)
feet_distance_y_exp = RewTerm(
func=mdp.feet_distance_y_exp,
weight=0.0,
params={
"std": math.sqrt(0.25),
"asset_cfg": SceneEntityCfg("robot", body_names=""),
"stance_width": float,
},
)
# feet_distance_xy_exp = RewTerm(
# func=mdp.feet_distance_xy_exp,
# weight=0.0,
# params={
# "std": math.sqrt(0.25),
# "asset_cfg": SceneEntityCfg("robot", body_names=""),
# "stance_length": float,
# "stance_width": float,
# },
# )
upward = RewTerm(func=mdp.upward, weight=0.0)
# lin_vel_xy_l2_with_ang_z_command = RewTerm(
# func=mdp.lin_vel_xy_l2_with_ang_z_command,
# weight=0,
# params={
# "command_name": "base_velocity",
# "command_threshold": 0.1,
# },
# ) # negetive
@configclass
class TerminationsCfg:
"""Termination terms for the MDP."""
# MDP terminations
time_out = DoneTerm(func=mdp.time_out, time_out=True)
# command_resample
terrain_out_of_bounds = DoneTerm(
func=mdp.terrain_out_of_bounds,
params={"asset_cfg": SceneEntityCfg("robot"), "distance_buffer": 3.0},
time_out=True,
)
# Contact sensor
illegal_contact = DoneTerm(
func=mdp.illegal_contact,
params={"sensor_cfg": SceneEntityCfg("contact_forces", body_names=""), "threshold": 1.0},
)
bad_orientation_2 = DoneTerm(func=mdp.bad_orientation_2)
@configclass
class CurriculumCfg:
"""Curriculum terms for the MDP."""
terrain_levels = CurrTerm(func=mdp.terrain_levels_vel)
command_levels = CurrTerm(
func=mdp.command_levels_vel,
params={
"reward_term_name": "track_lin_vel_xy_exp",
"range_multiplier": (0.1, 1.0),
},
)
##
# Environment configuration
##
@configclass
class LocomotionVelocityRoughEnvCfg(ManagerBasedRLEnvCfg):
"""Configuration for the locomotion velocity-tracking environment."""
# Scene settings
scene: MySceneCfg = MySceneCfg(num_envs=4096, env_spacing=2.5)
# Basic settings
observations: ObservationsCfg = ObservationsCfg()
actions: ActionsCfg = ActionsCfg()
commands: CommandsCfg = CommandsCfg()
# MDP settings
rewards: RewardsCfg = RewardsCfg()
terminations: TerminationsCfg = TerminationsCfg()
events: EventCfg = EventCfg()
curriculum: CurriculumCfg = CurriculumCfg()
def __post_init__(self):
"""Post initialization."""
# general settings
self.decimation = 4
self.episode_length_s = 20.0
# simulation settings
self.sim.dt = 0.005
self.sim.render_interval = self.decimation
self.sim.physics_material = self.scene.terrain.physics_material
self.sim.physx.gpu_max_rigid_patch_count = 10 * 2**15
self.sim.physx.max_position_iteration_count = 4
self.sim.physx.max_velocity_iteration_count = 1
# update sensor update periods
# we tick all the sensors based on the smallest update period (physics update period)
if self.scene.height_scanner is not None:
self.scene.height_scanner.update_period = self.decimation * self.sim.dt
if self.scene.contact_forces is not None:
self.scene.contact_forces.update_period = self.sim.dt
# check if terrain levels curriculum is enabled - if so, enable curriculum for terrain generator
# this generates terrains with increasing difficulty and is useful for training
if getattr(self.curriculum, "terrain_levels", None) is not None:
if self.scene.terrain.terrain_generator is not None:
self.scene.terrain.terrain_generator.curriculum = True
else:
if self.scene.terrain.terrain_generator is not None:
self.scene.terrain.terrain_generator.curriculum = False
def disable_zero_weight_rewards(self):
"""If the weight of rewards is 0, set rewards to None"""
for attr in dir(self.rewards):
if not attr.startswith("__"):
reward_attr = getattr(self.rewards, attr)
if not callable(reward_attr) and reward_attr.weight == 0:
setattr(self.rewards, attr, None)
def create_obsgroup_class(class_name, terms, enable_corruption=False, concatenate_terms=True):
"""
Dynamically create and register a ObsGroup class based on the given configuration terms.
:param class_name: Name of the configuration class.
:param terms: Configuration terms, a dictionary where keys are term names and values are term content.
:param enable_corruption: Whether to enable corruption for the observation group. Defaults to False.
:param concatenate_terms: Whether to concatenate the observation terms in the group. Defaults to True.
:return: The dynamically created class.
"""
# Dynamically determine the module name
module_name = inspect.getmodule(inspect.currentframe()).__name__
# Define the post-init function
def post_init_wrapper(self):
setattr(self, "enable_corruption", enable_corruption)
setattr(self, "concatenate_terms", concatenate_terms)
# Dynamically create the class using ObsGroup as the base class
terms["__post_init__"] = post_init_wrapper
dynamic_class = configclass(type(class_name, (ObsGroup,), terms))
# Custom serialization and deserialization
def __getstate__(self):
state = self.__dict__.copy()
return state
def __setstate__(self, state):
self.__dict__.update(state)
# Add custom serialization methods to the class
dynamic_class.__getstate__ = __getstate__
dynamic_class.__setstate__ = __setstate__
# Place the class in the global namespace for accessibility
globals()[class_name] = dynamic_class
# Register the dynamic class in the module's dictionary
if module_name in sys.modules:
sys.modules[module_name].__dict__[class_name] = dynamic_class
else:
raise ImportError(f"Module {module_name} not found.")
# Return the class for external instantiation
return dynamic_class
@@ -0,0 +1,52 @@
# Copyright (c) 2025 Deep Robotics
# SPDX-License-Identifier: BSD 3-Clause
#
# # Copyright (c) 2024-2025 Ziqi Fan
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) 2022-2025, The Isaac Lab Project Developers.
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
import omni.ext
# 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("[rl_training] 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("[rl_training] 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("[rl_training] shutdown")
+55
View File
@@ -0,0 +1,55 @@
# Copyright (c) 2025 Deep Robotics
# SPDX-License-Identifier: BSD 3-Clause
#
# # Copyright (c) 2024-2025 Ziqi Fan
# SPDX-License-Identifier: Apache-2.0
"""Installation script for the 'rl_training' 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 = [
# base
"psutil",
"colorama",
"xacrodoc",
# amp
"numpy",
"pandas",
"pinocchio",
# rl
"cusrl[all]",
]
# Installation operation
setup(
name="rl_training",
packages=["rl_training"],
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 License 2.0",
include_package_data=True,
python_requires=">=3.10",
classifiers=[
"Natural Language :: English",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Isaac Sim :: 4.5.0",
"Isaac Sim :: 5.0.0",
],
zip_safe=False,
)