This commit is contained in:
wty-yy
2025-12-29 16:33:09 +08:00
parent 83952bd549
commit 0c56db7e42
96 changed files with 770085 additions and 1 deletions

168
.gitignore vendored Normal file
View File

@@ -0,0 +1,168 @@
*.pt
MUJOCO_LOG.TXT
*.mp4
videos
images
mytest*
data*
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
.pdm.toml
.pdm-python
.pdm-build/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
venv/
ENV/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# Editors
.idea/
.vscode
# logs
logs

29
LICENSE
View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2025 wty
Copyright (c) 2025 Wu Tianyang
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -19,3 +19,30 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
This project is based on or incorporates code from https://github.com/unitreerobotics/unitree_rl_gym,
which is licensed under the BSD 3-Clause License:
BSD 3-Clause License
Copyright (c) 2016-2023 HangZhou YuShu TECHNOLOGY CO.,LTD. ("Unitree Robotics")
All rights reserved.
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.

152
README.md Normal file
View File

@@ -0,0 +1,152 @@
<div align="center">
<h1 align="center">Go2 RL GYM</h1>
<p align="center">
<a href="README_en.md">🌎 English</a> | <span>🇨🇳 中文</span>
</p>
</div>
<p align="center">
<strong>本仓库基于<a href="https://github.com/unitreerobotics/unitree_rl_gym">unitree_rl_gym</a>使用强化学习训练Go2机器狗。</strong>
</p>
<div align="center">
| <div align="center"> Isaac Gym </div> | <div align="center"> Mujoco </div> | <div align="center"> Physical </div> |
|--- | --- | --- |
| TODO | TODO | TODO |
</div>
## 📦 安装配置
安装和配置步骤请参考 [setup.md](/doc/setup_zh.md)
## 🛠️ 使用指南
### 1. 训练
运行以下命令进行训练:
```bash
python legged_gym/scripts/train.py --task=xxx
```
#### ⚙️ 参数说明
- `--task`: 必选参数,值可选(go2, go2_cts, go2_moe_cts)
- `--headless`: 默认启动图形界面,设为 true 时不渲染图形界面(效率更高)
- `--resume`: 从日志中选择 checkpoint 继续训练
- `--experiment_name`: 运行/加载的 experiment 名称
- `--run_name`: 运行/加载的 run 名称
- `--load_run`: 加载运行的名称,默认加载最后一次运行
- `--checkpoint`: checkpoint 编号,默认加载最新一次文件
- `--num_envs`: 并行训练的环境个数
- `--seed`: 随机种子
- `--max_iterations`: 训练的最大迭代次数
- `--sim_device`: 仿真计算设备,指定 CPU 为 `--sim_device=cpu`
- `--rl_device`: 强化学习计算设备,指定 CPU 为 `--rl_device=cpu`
**默认保存训练结果**`logs/<experiment_name>/<date_time>_<run_name>/model_<iteration>.pt`
---
### 2. Play
如果想要在 Gym 中查看训练效果,可以运行以下命令:
```bash
python legged_gym/scripts/play.py --task=xxx
```
**说明**
- Play 启动参数为随机地形难度在7到9之间。
- 默认加载实验文件夹最新训练的一个模型。
- 可通过 `experiment_name``checkpoint` 指定其他模型,例如
```bash
python legged_gym/scripts/play.py --task=go2_cts --num_envs 100 --experiment_name go2_cts_hard_terrain --checkpoint 100000
```
#### 💾 导出网络
Play 会导出 Actor 网络,保存于 `logs/{experiment_name}/exported/policies` 中:
- `policy.pt`: torch script模型用于Sim2Sim。
- `policy.onnx`: onnx模型用于Sim2Real。
- `policy.pkl`: 模型权重。
#### Play 效果
| Go2 |
|--- |
| TODO |
---
### 3. Sim2Sim (Mujoco)
支持在 Mujoco 仿真器中运行 Sim2Sim
```bash
python deploy/deploy_mujoco/deploy_go2.py
```
如果有xbox协议的手柄接入主机自动切换为手柄控制否则只会保持默认指令前进。
- 替换网络模型:默认模型位于 `deploy/pre_train/go2/go2_cts_150k.pt`;自己训练模型保存于`logs/{experiment_name}/exported/policies/policy.pt`,只需替换 yaml 配置文件中 `policy_path`。
- 替换环境地形:默认地形为 `resources/robots/go2/stairs.xml`,其他可选地形,平地 `flat.xml`,赛道 `race_track.xml`,地形使用[terrain_generator.py](resources/robots/go2/terrain_generator.py)生成,参考[unitree_mujoco/terrain_tool](https://github.com/unitreerobotics/unitree_mujoco/tree/main/terrain_tool)。
#### 运行效果
| 平地 | 台阶 | 赛道 |
|--- | --- | --- |
| | | |
---
### 4. Sim2Real
#### 4.1 Python实物部署 (需要安装 unitree_sdk2_python
先用app进入设备→服务状态→点击运控服务关闭`mcf`,打开`ota_box`服务。
假设和下位机连接的网卡名称为`eth0`,执行
```bash
cd deploy/deploy_real
python deploy_real_go2.py eth0
```
`start`站立,`A`启动控制
#### 4.2 C++实物部署(需要安装 unitree_cpp_deploy
参考[unitree_cpp_deploy](https://github.com/wty-yy-mini/unitree_cpp_deploy)使用说明。
#### 运行效果
| Python部署 | C++部署 |
| --- | --- |
| | |
---
## 🎉 致谢
本仓库开发离不开以下开源项目的支持与贡献,特此感谢:
- [unitree_rl_gym](https://github.com/unitreerobotics/unitree_rl_gym):宇树机器人强化学习训练基础框架。
- [legged\_gym](https://github.com/leggedrobotics/legged_gym):构建基础训练环境。
- [rsl\_rl](https://github.com/leggedrobotics/rsl_rl.git):强化学习算法实现。
- [mujoco](https://github.com/google-deepmind/mujoco.git)提供强大CPU仿真功能。
- [unitree\_sdk2\_python](https://github.com/unitreerobotics/unitree_sdk2_python.git)实物部署硬件Python通信接口。
- [unitree_sdk2](https://github.com/unitreerobotics/unitree_sdk2)实物部署硬件C++通信接口。
本仓库实现包含以下论文,特此感谢:
- [CTS: Concurrent Teacher-Student Reinforcement Learning for Legged Locomotion](https://arxiv.org/pdf/2405.10830)
---
## 🔖 许可证
新增内容根据 [MIT License](./LICENSE) 授权原仓库unitree_rl_gym根据 [BSD 3-Clause License](./LICENSE) 授权。
详情请阅读完整 [LICENSE 文件](./LICENSE)。

11
README_en.md Normal file
View File

@@ -0,0 +1,11 @@
<div align="center">
<h1 align="center">Go2 RL GYM</h1>
<p align="center">
<span> 🌎English </span> | <a href="README.md"> 🇨🇳中文 </a>
</p>
</div>
<p align="center">
<strong>This is a repository for reinforcement learning implementation based on Unitree Go2. Based on <a href="https://github.com/unitreerobotics/unitree_rl_gym">unitree_rl_gym</a>.
</strong>
</p>

126
UPDATE.md Normal file
View File

@@ -0,0 +1,126 @@
# 20251221
1. 修改最大地形速度限制, y在所有地形上最大为1.0, z只有平地最大为2.0, x最大为2.0
2. 上调难度9地形难度 (都是moe-cts 100k能通过的难度):
1. Slope: 19.8度 -> 29.6度
2. Stairs height: 0.212m -> 0.257m
3. Obstacles height: 0.23m -> 0.275m
# 20251204
1. 添加不同地形的最大速度限制
2. 加入`legs_distance`惩罚两腿横向距离过近的问题, 通过robogauge指标验证发现没有提升, 删除
# 20251128
1. go2.xml添加侧向和背面初始化 (需手动解开注释)
2. cts, moe-cts算法中添加norm_layer选项, 将latent_dim提升到512, norm_layer改为simnorm (dim=8)
3. 默认的layer norm还是用l2norm, latent_dim=32, 地形使用偏平地的地形, 因为开悟测评的分数还是更高, 平地也能走的更好
# 20251127
1. 修改deploy_go2.py支持xbox手柄指令输入
2. 修改turn_over开关, 默认关闭
# 20251126
## v0.1.11
1. 加入`turn_over`具体内容如下 (翻身roll为180度, 侧身roll为90度, 随机两个方向):
1. `init_state.turn_over_proportions`: 支持初始化时, 处于翻身/侧身的概率分别为0.1/0.3
2. `init_state.turn_over_init_heights`: 翻身/侧身的机的初始高度范围, 在该范围中随机采样
3. `commands.turn_over_zero_time`: 翻身和侧身翻转过来预计用时, 在这段时间内给出的command都是全零
4. `rewards.turn_over_roll_threshold`: 翻身奖励计算的roll阈值, 当roll.abs小于该阈值时转为计算原奖励
5. `reward.turn_over_scales`: 翻身奖励配置, 目前只使用了`_reward_upright`奖励计算负base的重力投影z分量和z轴单位向量做差, 系数1.0
2. 在环境数为4096时, 如果想达到8192的训练效果(尤其是上楼梯), 需要增大num_steps_per_env, 训4096时先加到48
3. 发现部分地形分数较低: wave, stairs up, down, 修改地形比例配置, 增大这三个地形, 降低flat, slop
4. 一开始从翻身开始是学不到, 只能从侧身开始学
# 20251125
## v0.1.10
1. 添加robot_properties的低频域随机化更新 (后来发现无法使用, 会导致机器人本体信息错误, 删除)
2. 消融修改num_steps_per_env=48,72,96
# 20251124
## v0.1.9
1. 修改action delay从0/1进行延迟, 改为{0,5,10,15}ms上进行延迟
2. 修改motor_offset和pd的域随机化从开始一次, 改为每次reset环境时随机
# 20251120
## v0.1.8
1. friction为0-2有提升, 在瓷砖地上可以上楼梯了
2. 将dynamic_sigma线速度最大值调到1.5, max_sigma等比例增大
3. rsl_rl支持保存全部的env_cfg和train_cfg配置文件, 便于参数检查
4. 加入MoE-CTS算法, 新任务go2_moe_cts, 消融load_balance_loss正则损失
# 20251119
## v0.1.7
1. 修复rsl_rl中剩余训练时间计算的小bug
2. 发现correct_base_height的奖励过小, 只有1e-3级别, 其他都是1e-2的, 可能导致滑倒时的重心偏移, 加入0-1500训练步的奖励课程系数1-10变化
3. 总训练步数调到50k
4. 域随机化: restitutionb不变0-0.5(isaacgym计算弹性系数是(地形0+机械人弹性系数)/2, 所以也不会很大), friction扩大到0.3-1.7 -> 0-2 (isaacgym中的摩擦计算方法是(地形1+机械人摩擦)/2, 因此可以开的范围更大点模仿瓷砖地)
5. 边界速度对向速度变换: 当连续两次切换边界速度时, 直接将边界速度取反
6. exporter加入新的pkl模型参数导出功能
# 20251118
## v0.1.6
1. 修改边界速度采样, 启用x正/负方向和y正/负方向最大值采样
2. 将heading_commands的角速度最大速度设置为当前的角速度范围 (原来是-1,1)
3. 加入command_range_curriculum在20000步时将最大速度范围修改为-1,1
4. 加入dynamic_sigma动态调整追踪奖励系数:
设动态调整$\sigma$的速度绝对值范围为$[v_{min}, v_{max}]$(角速度同理),第$i$种地形在最大速度下的速度追踪系数记为$\sigma_{max}^{terrain_i}$,则当指令速度为$v_{x}$y轴方向速度同理在第$i$种地形下时,当前系数为
$$\sigma_{vel} = \begin{cases}
\sigma,&\quad v_x\in[0,v_{min}),\\
\sigma(v_x-v_{min})+\sigma_{max}^{terrain_i}(v_{max}-v_{xy}),&\quad v_x\in[v_{min},v_{max}),\\
\sigma_{max}^{terrain_i},&\quad v_x\in(v_{max},\infty).
\end{cases}$$
$$\sigma_{now}=\sigma+\min(e^{\frac{\text{level}_i+1}{10}}-1,1)(\sigma_{vel}-\sigma)$$
5. 上真机发现在1.0速度时突变速度可能发生打滑的问题, 参考CTS论文给到`[0.3,1.7]`, 并消融`[0.5, 3.0]`
6. 真机的原地转向效果很差, 没有到1 rad/s, 关闭heading_command, 并将原来的limit_lin_vel改为limit_vel, 其中加入对最大角速度的最大最小和0的组合; 在原地禁止时, 加入20%的概率已边界角速度原地旋转
# 20251117
## v0.1.5
1. 加入动态调整resample command逻辑, 保证xy线速度能够出地形的一半距离:
设当前是$n_r$次采样指令,第$i$次的xy方向命令为$c_i^{xy}$$T_r$ 为采样间隔时间,$T$为episode时间则第$n_r+1$次命令最小xy速度为
$$v_{min} = \text{clip}\left(\frac{5-||\sum_{i}^{n_r}c_i^{xy}||_2T_r}{T-n_rT_r},0,v_{max}^{x\ or\ y}\right)$$
第$n_r+1$次有$p_{zero}$概率(课程增加到0.1)使xy零速度其持续时长为
$$T^{zero} = \text{clip}\left(T-n_rT_r-\frac{5-||\sum_{i}^{n_r}c_i^{xy}||_2T_r}{0.8\times\max(v^{max}_x,v^{max}_y)},0,T_r\right)$$
2. 移除当翻滚和俯仰导致的异常终止
3. 修改地形等级变化的distance, 为当前episodex距离中心点最大的distance
4. 加入地形之间的terrain_spacing=0.5m
5. 之前训练中将上下楼梯比例反了
6. 重新打开heading_command范围(-pi,pi), 并在reset时随机朝向
7. 调整地形比例降低wave, 更偏向平地斜坡
8. 加入一种边界线速度采样, 对x正方向和y正负方向最大值采样, 概率0.1, 当才到边界速度时取消角速度和heading指令
9. 加入域随机化:
1. `randomize_restitution=[0, 0.2]`机器人的弹性系数, 仅在初始化机器人时计算
2. `randomize_motor_strength=[0.8, 1.2]`机器人随机电机力矩系数, 仅在env.reset时计算
3. `randomize_action_delay`以0.5概率使用last_action, 每个env.step都计算
10. 提高域随机化`friction_range=[1, 5]`能明显提高真机前进效果 (消融了`[0.5, 3]`, 结果接近, 在变速时可能摔倒)
11. obs中加入`feet_contact_forces, dof_torques, dof_acceleration: 4, 12, 12`
# 20251116
## v0.1.4
1. 用平地奖励训练出来的模型会有x速度过大产生高抬腿动作, 同时给xy也会产生高抬腿, 重心偏移摔倒的问题, 说明加入更多的域随机化也没解决
2. 发现用CTS训练的模型能在mujoco中上20cm台阶, 但是由于双腿之间间距过小导致真机迁移效果非常差, 完全无法正常移动, 但是也能爬箱子
3. 考虑用CTS奖励进行训练, 加上similar_to_default或hip_to_default, 消融两种奖励, 以及对应的奖励系数, 0.01还是0.05
4. 真机上有明显的跺脚动作, 加上feet_contact_forces惩罚接触力大小, 设置接触力阈值大小为go2的重量15*9.8=147, 系数为-1 (参考collision)
5. 修复resume时第一个训练step前奖励系数没更新的bug
6. 关闭在x,y速度小于0.2时变为0, 加入zero_commands的课程, 0->1500训练步, 逐渐从0.0->0.1
7. 降低训练的command采样范围`x,y:[-1,1]->[-0.5,0.5]`
8. resample command降低`10->5`
# 20251115
## v0.1.3
1. 修改`terrain_level`计算方法, 取当前全部环境等级取平均
2. 仍然不使用`heading_command`, 虽然能够更稳定的提升环境等级, 并可以采样到更多的角速度指令, 但是无法以一个恒定的角速度进行移动, 和实际操作中不符
3. 修复`torch.jit`导出问题
4. 分别记录每个地形的奖励
5. 加载训练模型时, 支持环境奖励系数课程加载
6. 参考[yusongmin1-My_unitree_go2_gym](https://github.com/yusongmin1/My_unitree_go2_gym)加入域随机化`randomize_link_mass, randomize_base_com, randomize_pd_gains, randomize_motor_zero_offset`, 降低base_mass的最大值`3->1`, 降低`push_robots`xy方向速度`1.0->0.4`, 加入角速度推力`0.6`, 增大摩擦最小值`0.1->0.2`
7. 修改范围`lin_vel_y: 0.5->1.0`, 修改命令采样时间`resampling_time: 30->10`
8. 修改`base_height`计算使用的点云范围, 长宽`0.7x0.5->0.4x0.3`
9. 修改`wave, stairs`训练配置, 其中都加入`0.1`的平地
10. 关闭对`correct_base_height, dof_power`奖励的课程降低
11. 修改obs中`height_measurements`系数`5 -> 2.5`
# 20251114
## v0.1.2
1. 解决CTS`rollout_storage_cts.py`中学生教授数据采样混乱的问题
# 20251113
## v0.1.1
1. 上下楼梯环境训练7h13min完成, 纯PPO无法学到上楼梯动作, 下楼梯基本能完成
2. 测试10k的wave, slope, rough_slope训练, 修复地形提升问题
3. play.py中加入onnx模型导出功能
4. 机身高度稍微有点低, 提升3cm, `base_height_target: 0.35 -> 0.38`
5. 加入CTS算法替代PPO:
- 新文件: `on_policy_runner_cts.py, actor_critic_cts.py, cts.py, rollout_storage_cts.py`
- 新配置: `LeggedRobotCfgCTS`
- 新任务: `go2_cts`
- 新导出: 修改torch.script和onnx导出代码, onnx模型的输入是按照IsaacLab的按照item堆叠的结果, 部署C++代码的帧堆叠[obsevation_manager.h](https://github.com/unitreerobotics/unitree_rl_lab/blob/61bfba15d35f1a93e3bacab85fe06b31643c83b7/deploy/include/isaaclab/manager/observation_manager.h#L63)
## v0.1
1. 添加地形选择`wave, slope, rough_slope, stairs down, stairs up, obstacles, stepping_stones, gap, flat`
2. 添加高度特征
3. 修改base_height计算方法, 通过高度特征平均值计算
4. 支持课程奖励系数`rewards.curriculum_rewards`

15
cmd.md Normal file
View File

@@ -0,0 +1,15 @@
## Train
```bash
python legged_gym/scripts/train.py --task=go2 --num_envs 4096 --headless
python legged_gym/scripts/train.py --task=go2 --num_envs 128 --resume --load_run Nov13_11-14-22_wave_slope_rough_slope
python legged_gym/scripts/train.py --task=go2 --num_envs 8 # DEBUG
# CTS
python legged_gym/scripts/train.py --task=go2_cts --num_envs 8096 --headless
```
## Play
```bash
python legged_gym/scripts/play.py --task=go2 --num_envs 8 # load latest
python legged_gym/scripts/play.py --task=go2 --num_envs 8 --load_run Nov13_00-00-05_ # load specified run
# CTS
python legged_gym/scripts/play.py --task=go2_cts --num_envs 8
```

View File

@@ -0,0 +1,61 @@
policy_path: "{LEGGED_GYM_ROOT_DIR}/deploy/pre_train/go2/go2_cts_150k.pt"
# xml_path: "{LEGGED_GYM_ROOT_DIR}/resources/robots/go2/flat.xml"
# xml_path: "{LEGGED_GYM_ROOT_DIR}/resources/robots/go2/race_track.xml" # change go2 init pos to pos="-5 2 0.445" in resources/robots/go2/go2.xml
xml_path: "{LEGGED_GYM_ROOT_DIR}/resources/robots/go2/stairs.xml"
# Total simulation time
simulation_duration: 60000000.0
# Simulation time step
simulation_dt: 0.002
# Controller update frequency (meets the requirement of simulation_dt * controll_decimation=0.02; 50Hz)
control_decimation: 10
kps: [20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0]
kds: [0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5]
default_angles: [0.1, 0.8, -1.5, -0.1, 0.8, -1.5,
0.1, 1.0, -1.5, -0.1, 1.0, -1.5]
lin_vel_scale: 2.0
ang_vel_scale: 0.25
dof_pos_scale: 1.0
dof_vel_scale: 0.05
action_scale: 0.25
cmd_scale: [2.0, 2.0, 0.25]
num_actions: 12
num_obs: 45
max_cmd: [2.0, 1.5, 2.5]
cmd_init: [1.0, 0.0, 0.0]
mujoco_joint_names: [
"FL_hip_joint",
"FL_thigh_joint",
"FL_calf_joint",
"FR_hip_joint",
"FR_thigh_joint",
"FR_calf_joint",
"RL_hip_joint",
"RL_thigh_joint",
"RL_calf_joint",
"RR_hip_joint",
"RR_thigh_joint",
"RR_calf_joint"
]
# IsaacGym is same as Mujoco joint names
model_joint_names: [
"FL_hip_joint",
"FL_thigh_joint",
"FL_calf_joint",
"FR_hip_joint",
"FR_thigh_joint",
"FR_calf_joint",
"RL_hip_joint",
"RL_thigh_joint",
"RL_calf_joint",
"RR_hip_joint",
"RR_thigh_joint",
"RR_calf_joint"
]

View File

@@ -0,0 +1,251 @@
import time
import mujoco.viewer
import mujoco
import numpy as np
from legged_gym import LEGGED_GYM_ROOT_DIR
import torch
import yaml
import os
import imageio
from pathlib import Path
from argparse import ArgumentParser
import pygame
from matplotlib import pyplot as plt
def get_gravity_orientation(quaternion):
qw = quaternion[0]
qx = quaternion[1]
qy = quaternion[2]
qz = quaternion[3]
gravity_orientation = np.zeros(3)
gravity_orientation[0] = 2 * (-qz * qx + qw * qy)
gravity_orientation[1] = -2 * (qz * qy + qw * qx)
gravity_orientation[2] = 1 - 2 * (qw * qw + qz * qz)
return gravity_orientation
def pd_control(target_q, q, kp, target_dq, dq, kd):
"""Calculates torques from position commands"""
return (target_q - q) * kp + (target_dq - dq) * kd
def get_xbox_command(joystick, max_cmd):
pygame.event.pump()
dead_zone = 0.1
lx = joystick.get_axis(0)
ly = joystick.get_axis(1)
rx = joystick.get_axis(3)
if abs(lx) < dead_zone: lx = 0
if abs(ly) < dead_zone: ly = 0
if abs(rx) < dead_zone: rx = 0
cmd_x = -ly * max_cmd[0]
cmd_y = -lx * max_cmd[1]
cmd_yaw = -rx * max_cmd[2]
return np.array([cmd_x, cmd_y, cmd_yaw], dtype=np.float32)
if __name__ == "__main__":
parser = ArgumentParser()
parser.add_argument("--save-video", action="store_true", help="Whether to save video of the simulation.")
parser.add_argument("--visualize-moe-weights", action="store_true", help="Whether to visualize mixture of experts weights.")
parser.add_argument("--save-moe-latent", action="store_true", help="Whether to save mixture of experts latent vectors.")
args = parser.parse_args()
save_video = args.save_video
visualize_moe_weights = args.visualize_moe_weights
save_moe_latent = args.save_moe_latent
config_file = "go2.yaml"
pygame.init()
use_joystick = False
joystick = None
if pygame.joystick.get_count() > 0:
joystick = pygame.joystick.Joystick(0)
joystick.init()
use_joystick = True
print(f"Detected Joystick: {joystick.get_name()}")
else:
print("No Joystick detected. Using default commands from config.")
with open(f"{LEGGED_GYM_ROOT_DIR}/deploy/deploy_mujoco/configs/{config_file}", "r") as f:
config = yaml.load(f, Loader=yaml.FullLoader)
policy_path = config["policy_path"].replace("{LEGGED_GYM_ROOT_DIR}", LEGGED_GYM_ROOT_DIR)
xml_path = config["xml_path"].replace("{LEGGED_GYM_ROOT_DIR}", LEGGED_GYM_ROOT_DIR)
simulation_duration = config["simulation_duration"]
simulation_dt = config["simulation_dt"]
control_decimation = config["control_decimation"]
kps = np.array(config["kps"], dtype=np.float32)
kds = np.array(config["kds"], dtype=np.float32)
default_angles = np.array(config["default_angles"], dtype=np.float32)
lin_vel_scale = config["lin_vel_scale"]
ang_vel_scale = config["ang_vel_scale"]
dof_pos_scale = config["dof_pos_scale"]
dof_vel_scale = config["dof_vel_scale"]
action_scale = config["action_scale"]
cmd_scale = np.array(config["cmd_scale"], dtype=np.float32)
num_actions = config["num_actions"]
num_obs = config["num_obs"]
cmd = np.array(config["cmd_init"], dtype=np.float32)
idx_model2mj = idx_mj2model = list(range(num_actions))
if 'mujoco_joint_names' in config and 'model_joint_names' in config:
mujoco_joint_names = config["mujoco_joint_names"]
model_joint_names = config["model_joint_names"]
idx_model2mj = [model_joint_names.index(joint) for joint in mujoco_joint_names]
idx_mj2model = [mujoco_joint_names.index(joint) for joint in model_joint_names]
video_save_dir = str(Path(__file__).parent / "videos")
os.makedirs(video_save_dir, exist_ok=True)
model_name = os.path.basename(policy_path).split('.')[0]
cmd_str = f"cmd_{cmd[0]}_{cmd[1]}_{cmd[2]}"
# define context variables
action = np.zeros(num_actions, dtype=np.float32)
last_action = np.zeros(num_actions, dtype=np.float32)
target_dof_pos = default_angles.copy()
obs = np.zeros(num_obs, dtype=np.float32)
counter = 0
# Load robot model
m = mujoco.MjModel.from_xml_path(xml_path)
d = mujoco.MjData(m)
m.opt.timestep = simulation_dt
renderer = mujoco.Renderer(m, height=360, width=640)
# load policy
policy = torch.jit.load(policy_path)
if save_video:
video_filename = f"{model_name}_{cmd_str}.mp4"
video_path = os.path.join(video_save_dir, video_filename)
print(f"Video recording will be saved to: {video_path}")
video_fps = 50
sim_fps = 1.0 / m.opt.timestep
frame_skip = int(sim_fps / video_fps)
if frame_skip < 1:
frame_skip = 1
writer = imageio.get_writer(video_path, fps=video_fps)
print(f"Sim FPS: {sim_fps:.2f}, Video FPS: {video_fps}, Frame Skip: {frame_skip}, Save at: {video_path}")
if visualize_moe_weights:
plt.ion()
fig, ax = plt.subplots(figsize=(5,3))
ax.set_title(f"Command: Vx={cmd[0]:.2f}, Vy={cmd[1]:.2f}, Wz={cmd[2]:.2f}")
bars = None
if save_moe_latent:
latent_save_dir = str(Path(__file__).parent / "data_latents")
os.makedirs(latent_save_dir, exist_ok=True)
latent_filename = f"{model_name}_{cmd_str}_latents.npy"
latent_path = os.path.join(latent_save_dir, latent_filename)
all_latents = []
with mujoco.viewer.launch_passive(m, d) as viewer:
# set viewer.camera to follow robot
viewer.cam.type = mujoco.mjtCamera.mjCAMERA_TRACKING
viewer.cam.trackbodyid = 1
viewer.cam.distance = 3.0
viewer.cam.elevation = -30.0
viewer.cam.azimuth = 0.0
# Close the viewer automatically after simulation_duration wall-seconds.
start = time.time()
while viewer.is_running() and time.time() - start < simulation_duration:
step_start = time.time()
if use_joystick and counter % control_decimation == 0:
cmd = get_xbox_command(joystick, config["max_cmd"])
print(f"Cmd: Vx={cmd[0]:.2f}, Vy={cmd[1]:.2f}, Wz={cmd[2]:.2f}", end='\r')
tau = pd_control(target_dof_pos, d.qpos[7:], kps, np.zeros_like(kds), d.qvel[6:], kds)
d.ctrl[:] = tau
# mj_step can be replaced with code that also evaluates
# a policy and applies a control signal before stepping the physics.
mujoco.mj_step(m, d)
if save_video and counter % frame_skip == 0:
try:
renderer.update_scene(d, camera=viewer.cam)
frame = renderer.render()
writer.append_data(frame)
except Exception as e:
print(f"Error rendering frame: {e}")
counter += 1
if counter % control_decimation == 0:
# Apply control signal here.
# create observation
qj = d.qpos[7:]
dqj = d.qvel[6:]
quat = d.qpos[3:7]
lin_vel = d.qvel[:3]
ang_vel = d.qvel[3:6]
qj = (qj - default_angles) * dof_pos_scale
dqj = dqj * dof_vel_scale
gravity_orientation = get_gravity_orientation(quat)
lin_vel = lin_vel * lin_vel_scale
ang_vel = ang_vel * ang_vel_scale
obs[:3] = ang_vel
obs[3:6] = gravity_orientation
obs[6:9] = cmd * cmd_scale
obs[9 : 9 + num_actions] = qj[idx_mj2model]
obs[9 + num_actions : 9 + 2 * num_actions] = dqj[idx_mj2model]
obs[9 + 2 * num_actions : 9 + 3 * num_actions] = action[idx_mj2model]
obs_tensor = torch.from_numpy(obs).unsqueeze(0)
# policy inference
last_action = action
result = policy(obs_tensor)
if isinstance(result, tuple):
action, (weights, latent) = result # moe
action = action.detach().numpy().squeeze()[idx_model2mj]
weights = weights.detach().numpy().squeeze()
latent = latent.detach().numpy().squeeze()
if visualize_moe_weights:
if bars is None:
x = np.arange(len(weights))
bars = ax.bar(x, weights)
ax.set_ylim(0, 1)
else:
for bar, w in zip(bars, weights):
bar.set_height(w)
plt.draw()
plt.pause(0.001) # 这会造成大约 1ms 的延迟
if save_moe_latent:
all_latents.append(latent)
else:
action = result.detach().cpu().numpy().squeeze()[idx_model2mj]
# transform action to target_dof_pos
target_dof_pos = action * action_scale + default_angles
# Pick up changes to the physics state, apply perturbations, update options from GUI.
viewer.sync()
# Rudimentary time keeping, will drift relative to wall clock.
# time_until_next_step = m.opt.timestep - (time.time() - step_start) - 0.1
# if time_until_next_step > 0:
# time.sleep(time_until_next_step)
# writer.close()
if save_video:
print(f"Video saved successfully to {video_path}")
writer.close()
if save_moe_latent and len(all_latents) > 0:
all_latents = np.array(all_latents)
np.save(latent_path, all_latents)
print(f"Latent vectors saved successfully to {latent_path}")

View File

@@ -0,0 +1,126 @@
import time
import mujoco.viewer
import mujoco
import numpy as np
from legged_gym import LEGGED_GYM_ROOT_DIR
import torch
import yaml
def get_gravity_orientation(quaternion):
qw = quaternion[0]
qx = quaternion[1]
qy = quaternion[2]
qz = quaternion[3]
gravity_orientation = np.zeros(3)
gravity_orientation[0] = 2 * (-qz * qx + qw * qy)
gravity_orientation[1] = -2 * (qz * qy + qw * qx)
gravity_orientation[2] = 1 - 2 * (qw * qw + qz * qz)
return gravity_orientation
def pd_control(target_q, q, kp, target_dq, dq, kd):
"""Calculates torques from position commands"""
return (target_q - q) * kp + (target_dq - dq) * kd
if __name__ == "__main__":
# get config file name from command line
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("config_file", type=str, help="config file name in the config folder")
args = parser.parse_args()
config_file = args.config_file
with open(f"{LEGGED_GYM_ROOT_DIR}/deploy/deploy_mujoco/configs/{config_file}", "r") as f:
config = yaml.load(f, Loader=yaml.FullLoader)
policy_path = config["policy_path"].replace("{LEGGED_GYM_ROOT_DIR}", LEGGED_GYM_ROOT_DIR)
xml_path = config["xml_path"].replace("{LEGGED_GYM_ROOT_DIR}", LEGGED_GYM_ROOT_DIR)
simulation_duration = config["simulation_duration"]
simulation_dt = config["simulation_dt"]
control_decimation = config["control_decimation"]
kps = np.array(config["kps"], dtype=np.float32)
kds = np.array(config["kds"], dtype=np.float32)
default_angles = np.array(config["default_angles"], dtype=np.float32)
joint_ids_map = config["joint_ids_map"]
ang_vel_scale = config["ang_vel_scale"]
dof_pos_scale = config["dof_pos_scale"]
dof_vel_scale = config["dof_vel_scale"]
action_scale = config["action_scale"]
cmd_scale = np.array(config["cmd_scale"], dtype=np.float32)
num_actions = config["num_actions"]
num_obs = config["num_obs"]
cmd = np.array(config["cmd_init"], dtype=np.float32)
# define context variables
action = np.zeros(num_actions, dtype=np.float32)
target_dof_pos = default_angles.copy()
obs = np.zeros(num_obs, dtype=np.float32)
counter = 0
# Load robot model
m = mujoco.MjModel.from_xml_path(xml_path)
d = mujoco.MjData(m)
m.opt.timestep = simulation_dt
# load policy
policy = torch.jit.load(policy_path)
with mujoco.viewer.launch_passive(m, d) as viewer:
# Close the viewer automatically after simulation_duration wall-seconds.
start = time.time()
while viewer.is_running() and time.time() - start < simulation_duration:
step_start = time.time()
temp = target_dof_pos[[0,4,8,1,5,9,2,6,10,3,7,11]]
tau = pd_control(temp, d.qpos[7:], kps, np.zeros_like(kds), d.qvel[6:], kds)
d.ctrl[:] = tau
# mj_step can be replaced with code that also evaluates
# a policy and applies a control signal before stepping the physics.
mujoco.mj_step(m, d)
counter += 1
if counter % control_decimation == 0:
# Apply control signal here.
# create observation
qj = d.qpos[7:]
dqj = d.qvel[6:]
quat = d.qpos[3:7]
ang_vel = d.qvel[3:6]
qj = (qj - default_angles) * dof_pos_scale
dqj = dqj * dof_vel_scale
gravity_orientation = get_gravity_orientation(quat)
ang_vel = ang_vel * ang_vel_scale
obs[:3] = ang_vel
obs[3:6] = gravity_orientation
obs[6:9] = cmd * cmd_scale
obs[9 : 9 + num_actions] = qj[joint_ids_map]
obs[9 + num_actions : 9 + 2 * num_actions] = dqj[joint_ids_map]
obs[9 + 2 * num_actions : 9 + 3 * num_actions] = action
obs_tensor = torch.from_numpy(obs).unsqueeze(0)
# policy inference
action = policy(obs_tensor).detach().numpy().squeeze()
# transform action to target_dof_pos
target_dof_pos = action * action_scale + default_angles[joint_ids_map]
# Pick up changes to the physics state, apply perturbations, update options from GUI.
viewer.sync()
# Rudimentary time keeping, will drift relative to wall clock.
time_until_next_step = m.opt.timestep - (time.time() - step_start)
if time_until_next_step > 0:
time.sleep(time_until_next_step)

View File

@@ -0,0 +1,272 @@
import time
import mujoco.viewer
import mujoco
import numpy as np
from legged_gym import LEGGED_GYM_ROOT_DIR
import torch
import yaml
import os
import imageio
from pathlib import Path
from argparse import ArgumentParser
import pygame
# from matplotlib import pyplot as plt # 移除 matplotlib
def get_gravity_orientation(quaternion):
qw = quaternion[0]
qx = quaternion[1]
qy = quaternion[2]
qz = quaternion[3]
gravity_orientation = np.zeros(3)
gravity_orientation[0] = 2 * (-qz * qx + qw * qy)
gravity_orientation[1] = -2 * (qz * qy + qw * qx)
gravity_orientation[2] = 1 - 2 * (qw * qw + qz * qz)
return gravity_orientation
def pd_control(target_q, q, kp, target_dq, dq, kd):
"""Calculates torques from position commands"""
return (target_q - q) * kp + (target_dq - dq) * kd
def get_xbox_command(joystick, max_cmd):
# 注意:如果开启了 Pygame 显示窗口,这里 event.pump 也是必要的
pygame.event.pump()
dead_zone = 0.1
if joystick is not None:
lx = joystick.get_axis(0)
ly = joystick.get_axis(1)
rx = joystick.get_axis(3)
if abs(lx) < dead_zone: lx = 0
if abs(ly) < dead_zone: ly = 0
if abs(rx) < dead_zone: rx = 0
cmd_x = -ly * max_cmd[0]
cmd_y = -lx * max_cmd[1]
cmd_yaw = -rx * max_cmd[2]
return np.array([cmd_x, cmd_y, cmd_yaw], dtype=np.float32)
return np.zeros(3, dtype=np.float32)
def draw_moe_weights(screen, weights, width, height):
"""使用 Pygame 绘制 MoE 权重"""
screen.fill((255, 255, 255)) # 白底
num_experts = len(weights)
if num_experts == 0:
return
# 设置边距
margin = 5
bar_width = (width - 2 * margin) / num_experts
max_bar_height = height - 2 * margin
for i, w in enumerate(weights):
# 限制 w 在 [0, 1] 之间用于显示
w_clamped = max(0.0, min(1.0, w))
bar_height = int(w_clamped * max_bar_height)
# 计算矩形位置 (Pygame 坐标原点在左上角)
# left, top, width, height
x = margin + i * bar_width
y = height - margin - bar_height # 从底部向上长
# 绘制矩形 (蓝色)
# 在 bar 之间留一点空隙 (width - 2)
pygame.draw.rect(screen, (50, 100, 255), (x, y, bar_width - 2, bar_height))
pygame.display.flip()
if __name__ == "__main__":
parser = ArgumentParser()
parser.add_argument("--save-video", action="store_true", help="Whether to save video of the simulation.")
parser.add_argument("--visualize-moe-weights", action="store_true", help="Whether to visualize mixture of experts weights.")
args = parser.parse_args()
save_video = args.save_video
visualize_moe_weights = args.visualize_moe_weights
config_file = "go2.yaml"
# Pygame 初始化
pygame.init()
use_joystick = False
joystick = None
if pygame.joystick.get_count() > 0:
joystick = pygame.joystick.Joystick(0)
joystick.init()
use_joystick = True
print(f"Detected Joystick: {joystick.get_name()}")
else:
print("No Joystick detected. Using default commands from config.")
# 如果需要可视化权重,设置 Pygame 窗口
screen = None
win_width, win_height = 400, 200
if visualize_moe_weights:
# 创建一个独立的窗口用于显示权重
screen = pygame.display.set_mode((win_width, win_height))
pygame.display.set_caption("MoE Weights Visualization")
with open(f"{LEGGED_GYM_ROOT_DIR}/deploy/deploy_mujoco/configs/{config_file}", "r") as f:
config = yaml.load(f, Loader=yaml.FullLoader)
policy_path = config["policy_path"].replace("{LEGGED_GYM_ROOT_DIR}", LEGGED_GYM_ROOT_DIR)
xml_path = config["xml_path"].replace("{LEGGED_GYM_ROOT_DIR}", LEGGED_GYM_ROOT_DIR)
simulation_duration = config["simulation_duration"]
simulation_dt = config["simulation_dt"]
control_decimation = config["control_decimation"]
kps = np.array(config["kps"], dtype=np.float32)
kds = np.array(config["kds"], dtype=np.float32)
default_angles = np.array(config["default_angles"], dtype=np.float32)
lin_vel_scale = config["lin_vel_scale"]
ang_vel_scale = config["ang_vel_scale"]
dof_pos_scale = config["dof_pos_scale"]
dof_vel_scale = config["dof_vel_scale"]
action_scale = config["action_scale"]
cmd_scale = np.array(config["cmd_scale"], dtype=np.float32)
num_actions = config["num_actions"]
num_obs = config["num_obs"]
cmd = np.array(config["cmd_init"], dtype=np.float32)
idx_model2mj = idx_mj2model = list(range(num_actions))
if 'mujoco_joint_names' in config and 'model_joint_names' in config:
mujoco_joint_names = config["mujoco_joint_names"]
model_joint_names = config["model_joint_names"]
idx_model2mj = [model_joint_names.index(joint) for joint in mujoco_joint_names]
idx_mj2model = [mujoco_joint_names.index(joint) for joint in model_joint_names]
video_save_dir = str(Path(__file__).parent / "videos")
os.makedirs(video_save_dir, exist_ok=True)
model_name = os.path.basename(policy_path).split('.')[0]
cmd_str = f"cmd_{cmd[0]}_{cmd[1]}_{cmd[2]}"
video_filename = f"{model_name}_{cmd_str}.mp4"
video_path = os.path.join(video_save_dir, video_filename)
print(f"Video recording will be saved to: {video_path}")
# define context variables
action = np.zeros(num_actions, dtype=np.float32)
last_action = np.zeros(num_actions, dtype=np.float32)
target_dof_pos = default_angles.copy()
obs = np.zeros(num_obs, dtype=np.float32)
counter = 0
# Load robot model
m = mujoco.MjModel.from_xml_path(xml_path)
d = mujoco.MjData(m)
m.opt.timestep = simulation_dt
renderer = mujoco.Renderer(m, height=360, width=640)
# load policy
policy = torch.jit.load(policy_path)
if save_video:
video_fps = 50
sim_fps = 1.0 / m.opt.timestep
frame_skip = int(sim_fps / video_fps)
if frame_skip < 1:
frame_skip = 1
writer = imageio.get_writer(video_path, fps=video_fps)
print(f"Sim FPS: {sim_fps:.2f}, Video FPS: {video_fps}, Frame Skip: {frame_skip}, Save at: {video_path}")
# 移除了 plt 初始化逻辑
with mujoco.viewer.launch_passive(m, d) as viewer:
# set viewer.camera to follow robot
viewer.cam.type = mujoco.mjtCamera.mjCAMERA_TRACKING
viewer.cam.trackbodyid = 1
viewer.cam.distance = 3.0
viewer.cam.elevation = -30.0
viewer.cam.azimuth = 0.0
# Close the viewer automatically after simulation_duration wall-seconds.
start = time.time()
while viewer.is_running() and time.time() - start < simulation_duration:
step_start = time.time()
if use_joystick and counter % control_decimation == 0:
cmd = get_xbox_command(joystick, config["max_cmd"])
print(f"Cmd: Vx={cmd[0]:.2f}, Vy={cmd[1]:.2f}, Wz={cmd[2]:.2f}", end='\r')
elif visualize_moe_weights and counter % control_decimation == 0:
# 如果没有手柄但开了可视化,也需要 pump 事件,防止窗口卡死
pygame.event.pump()
tau = pd_control(target_dof_pos, d.qpos[7:], kps, np.zeros_like(kds), d.qvel[6:], kds)
d.ctrl[:] = tau
mujoco.mj_step(m, d)
if save_video and counter % frame_skip == 0:
try:
renderer.update_scene(d, camera=viewer.cam)
frame = renderer.render()
writer.append_data(frame)
except Exception as e:
print(f"Error rendering frame: {e}")
counter += 1
if counter % control_decimation == 0:
# Apply control signal here.
# create observation
qj = d.qpos[7:]
dqj = d.qvel[6:]
quat = d.qpos[3:7]
lin_vel = d.qvel[:3]
ang_vel = d.qvel[3:6]
qj = (qj - default_angles) * dof_pos_scale
dqj = dqj * dof_vel_scale
gravity_orientation = get_gravity_orientation(quat)
lin_vel = lin_vel * lin_vel_scale
ang_vel = ang_vel * ang_vel_scale
obs[:3] = ang_vel
obs[3:6] = gravity_orientation
obs[6:9] = cmd * cmd_scale
obs[9 : 9 + num_actions] = qj[idx_mj2model]
obs[9 + num_actions : 9 + 2 * num_actions] = dqj[idx_mj2model]
obs[9 + 2 * num_actions : 9 + 3 * num_actions] = action[idx_mj2model]
obs_tensor = torch.from_numpy(obs).unsqueeze(0)
# policy inference
last_action = action
result = policy(obs_tensor)
# 处理 MoE 和 绘图
if isinstance(result, tuple):
action, weights = result # moe
action = action.detach().numpy().squeeze()[idx_model2mj]
weights = weights.detach().numpy().squeeze()
if visualize_moe_weights and screen is not None:
draw_moe_weights(screen, weights, win_width, win_height)
else:
action = result.detach().numpy().squeeze()[idx_model2mj]
# transform action to target_dof_pos
target_dof_pos = action * action_scale + default_angles
# Pick up changes to the physics state, apply perturbations, update options from GUI.
viewer.sync()
# 如果需要严格同步时间,可以解开下面的注释
# time_until_next_step = m.opt.timestep - (time.time() - step_start)
# if time_until_next_step > 0:
# time.sleep(time_until_next_step)
if save_video:
writer.close()
# 退出时清理 Pygame
pygame.quit()
print(f"Video saved successfully to {video_path}")

View File

@@ -0,0 +1,61 @@
from unitree_sdk2py.idl.unitree_go.msg.dds_ import LowCmd_ as LowCmdGo
from unitree_sdk2py.idl.unitree_hg.msg.dds_ import LowCmd_ as LowCmdHG
from typing import Union
class MotorMode:
PR = 0 # Series Control for Pitch/Roll Joints
AB = 1 # Parallel Control for A/B Joints
def create_damping_cmd(cmd: Union[LowCmdGo, LowCmdHG]):
size = len(cmd.motor_cmd)
for i in range(size):
cmd.motor_cmd[i].q = 0
cmd.motor_cmd[i].qd = 0
cmd.motor_cmd[i].kp = 0
cmd.motor_cmd[i].kd = 8
cmd.motor_cmd[i].tau = 0
def create_zero_cmd(cmd: Union[LowCmdGo, LowCmdHG]):
size = len(cmd.motor_cmd)
for i in range(size):
cmd.motor_cmd[i].q = 0
cmd.motor_cmd[i].qd = 0
cmd.motor_cmd[i].kp = 0
cmd.motor_cmd[i].kd = 0
cmd.motor_cmd[i].tau = 0
def init_cmd_hg(cmd: LowCmdHG, mode_machine: int, mode_pr: int):
cmd.mode_machine = mode_machine
cmd.mode_pr = mode_pr
size = len(cmd.motor_cmd)
for i in range(size):
cmd.motor_cmd[i].mode = 1
cmd.motor_cmd[i].q = 0
cmd.motor_cmd[i].qd = 0
cmd.motor_cmd[i].kp = 0
cmd.motor_cmd[i].kd = 0
cmd.motor_cmd[i].tau = 0
def init_cmd_go(cmd: LowCmdGo, weak_motor: list):
cmd.head[0] = 0xFE
cmd.head[1] = 0xEF
cmd.level_flag = 0xFF
cmd.gpio = 0
PosStopF = 2.146e9
VelStopF = 16000.0
size = len(cmd.motor_cmd)
for i in range(size):
if i in weak_motor:
cmd.motor_cmd[i].mode = 1
else:
cmd.motor_cmd[i].mode = 0x0A
cmd.motor_cmd[i].q = PosStopF
cmd.motor_cmd[i].qd = VelStopF
cmd.motor_cmd[i].kp = 0
cmd.motor_cmd[i].kd = 0
cmd.motor_cmd[i].tau = 0

View File

@@ -0,0 +1,39 @@
import struct
class KeyMap:
R1 = 0
L1 = 1
start = 2
select = 3
R2 = 4
L2 = 5
F1 = 6
F2 = 7
A = 8
B = 9
X = 10
Y = 11
up = 12
right = 13
down = 14
left = 15
class RemoteController:
def __init__(self):
self.lx = 0
self.ly = 0
self.rx = 0
self.ry = 0
self.button = [0] * 16
def set(self, data):
# wireless_remote
keys = struct.unpack("H", data[2:4])[0]
for i in range(16):
self.button[i] = (keys & (1 << i)) >> i
self.lx = struct.unpack("f", data[4:8])[0]
self.rx = struct.unpack("f", data[8:12])[0]
self.ry = struct.unpack("f", data[12:16])[0]
self.ly = struct.unpack("f", data[20:24])[0]

View File

@@ -0,0 +1,25 @@
import numpy as np
from scipy.spatial.transform import Rotation as R
def get_gravity_orientation(quaternion):
qw = quaternion[0]
qx = quaternion[1]
qy = quaternion[2]
qz = quaternion[3]
gravity_orientation = np.zeros(3)
gravity_orientation[0] = 2 * (-qz * qx + qw * qy)
gravity_orientation[1] = -2 * (qz * qy + qw * qx)
gravity_orientation[2] = 1 - 2 * (qw * qw + qz * qz)
return gravity_orientation
def transform_imu_data(waist_yaw, waist_yaw_omega, imu_quat, imu_omega):
RzWaist = R.from_euler("z", waist_yaw).as_matrix()
R_torso = R.from_quat([imu_quat[1], imu_quat[2], imu_quat[3], imu_quat[0]]).as_matrix()
R_pelvis = np.dot(R_torso, RzWaist.T)
w = np.dot(RzWaist, imu_omega[0]) - np.array([0, 0, waist_yaw_omega])
return R.from_matrix(R_pelvis).as_quat()[[3, 0, 1, 2]], w

View File

@@ -0,0 +1,35 @@
from legged_gym import LEGGED_GYM_ROOT_DIR
import numpy as np
import yaml
class Config:
def __init__(self, file_path) -> None:
with open(file_path, "r") as f:
config = yaml.load(f, Loader=yaml.FullLoader)
self.control_dt = config["control_dt"]
self.joint2motor_idx = config["joint2motor_idx"]
self.msg_type = config["msg_type"]
self.imu_type = config["imu_type"]
self.lowcmd_topic = config["lowcmd_topic"]
self.lowstate_topic = config["lowstate_topic"]
self.policy_path = config["policy_path"].replace("{LEGGED_GYM_ROOT_DIR}", LEGGED_GYM_ROOT_DIR)
self.kps = np.array(config["kps"],dtype=np.float32)
self.kds = np.array(config["kds"],dtype=np.float32)
self.default_angles = np.array(config["default_angles"], dtype=np.float32)
self.obs_scales_ang_vel = config["obs_scales_ang_vel"]
self.obs_scales_dof_pos = config["obs_scales_dof_pos"]
self.obs_scales_dof_vel = config["obs_scales_dof_vel"]
self.command_scale = config["command_scale"]
self.action_scale = config["action_scale"]
self.num_actions = config["num_actions"]
self.num_obs = config["num_obs"]

View File

@@ -0,0 +1,27 @@
control_dt: 0.02
msg_type: "go" # "hg" or "go"
imu_type: "torso" # "torso" or "pelvis"
lowcmd_topic: "rt/lowcmd"
lowstate_topic: "rt/lowstate"
policy_path: "{LEGGED_GYM_ROOT_DIR}/deploy/pre_train/go2/go2_cts_150k.pt"
joint2motor_idx: [3,4,5,0,1,2,9,10,11,6,7,8]
kps: [20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20]
kds: [0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5]
default_angles: [ 0.1, 0.8, -1.5,
-0.1, 0.8, -1.5,
0.1, 1.0, -1.5,
-0.1, 1.0, -1.5]
obs_scales_ang_vel: 0.25
obs_scales_dof_pos: 1.0
obs_scales_dof_vel: 0.05
command_scale: [3.0, 2.0, 0.5]
action_scale: 0.25
num_actions: 12
num_obs: 45

View File

@@ -0,0 +1,218 @@
from legged_gym import LEGGED_GYM_ROOT_DIR
import numpy as np
import time
import torch
from unitree_sdk2py.core.channel import ChannelPublisher,ChannelSubscriber,ChannelFactoryInitialize
from unitree_sdk2py.idl.default import unitree_go_msg_dds__LowCmd_,unitree_go_msg_dds__LowState_
from unitree_sdk2py.idl.unitree_go.msg.dds_ import LowCmd_ as LowCmdGo
from unitree_sdk2py.idl.unitree_go.msg.dds_ import LowState_ as LowStateGo
from unitree_sdk2py.utils.crc import CRC
from common.command_helper import create_zero_cmd,create_damping_cmd
from common.rotation_helper import get_gravity_orientation
from common.remote_controller import RemoteController, KeyMap
from config_go2 import Config
HIGHLEVEL = 0xEE
LOWLEVEL = 0xFF
TRIGERLEVEL = 0xF0
PosStopF = 2.146e9
VelStopF = 16000.0
def init_cmd_go2(cmd:LowCmdGo):
cmd.head[0] = 0xFE
cmd.head[1] = 0xEF
cmd.level_flag = 0xFF
cmd.gpio = 0
for i in range(12):
cmd.motor_cmd[i].mode = 0x0A # 0x01
cmd.motor_cmd[i].q = PosStopF
cmd.motor_cmd[i].dq = VelStopF # or qd
cmd.motor_cmd[i].kp = 0.0
cmd.motor_cmd[i].kd = 0.0
cmd.motor_cmd[i].tau = 0.0
class Controller:
def __init__(self,config:Config) -> None:
self.config = config
self.remote_controller = RemoteController()
self.use_remote_controller = True
self.policy = torch.jit.load(config.policy_path)
self._warm_up()
self.qj = np.zeros(config.num_actions,dtype=np.float32)
self.dqj = np.zeros(config.num_actions,dtype=np.float32)
self.action = np.zeros(config.num_actions,dtype=np.float32)
self.target_dof_pos = config.default_angles.copy()
self.obs = np.zeros(config.num_obs,dtype=np.float32)
self.cmd = np.array([0.8, 0, 0],dtype=np.float32)
self.counter = 0
self.low_cmd = unitree_go_msg_dds__LowCmd_()
self.low_state = unitree_go_msg_dds__LowState_()
self.lowcmd_publisher = ChannelPublisher(config.lowcmd_topic,LowCmdGo)
self.lowcmd_publisher.Init()
self.lowstate_subscriber = ChannelSubscriber(config.lowstate_topic,LowStateGo)
self.lowstate_subscriber.Init(self.LowStateHandler,10)
# self.replay_buffer = ReplayBuffer(max_replay_buffer_size=200,flag='real_new')
self.wait_for_low_state()
init_cmd_go2(self.low_cmd)
def _warm_up(self):
obs = torch.ones((1,45))
for _ in range(10):
_ = self.policy(obs)
print('Network has been warmed up.')
def wait_for_low_state(self):
while self.low_state.tick == 0:
time.sleep(self.config.control_dt)
print("Successfully connected to the robot.")
def LowStateHandler(self,msg:LowStateGo):
self.low_state = msg
self.remote_controller.set(self.low_state.wireless_remote)
def send_cmd(self,cmd:LowCmdGo):
cmd.crc = CRC().Crc(cmd)
self.lowcmd_publisher.Write(cmd)
def zero_torque_state(self):
print("Enter zero torque state.")
print("Waiting for the start signal...")
while self.remote_controller.button[KeyMap.start] != 1:
create_zero_cmd(self.low_cmd)
self.send_cmd(self.low_cmd)
time.sleep(self.config.control_dt)
def move_to_default_pos(self):
print('Moving to default pos.')
total_time = 2
num_step = int(total_time / self.config.control_dt)
dof_idx = self.config.joint2motor_idx
default_pos = self.config.default_angles
init_dof_pos = np.zeros(12,dtype=np.float32)
for i in range(12):
init_dof_pos[i] = self.low_state.motor_state[dof_idx[i]].q
for i in range(num_step):
alpha = i / num_step
for j in range(12):
motor_idx = dof_idx[j]
target_pos = default_pos[j]
self.low_cmd.motor_cmd[motor_idx].q = init_dof_pos[j] * (1 - alpha) + target_pos * alpha
self.low_cmd.motor_cmd[motor_idx].dq = 0.0 # qd
self.low_cmd.motor_cmd[motor_idx].kp = 40.0
self.low_cmd.motor_cmd[motor_idx].kd = 0.6
self.low_cmd.motor_cmd[motor_idx].tau = 0.0
self.send_cmd(self.low_cmd)
time.sleep(self.config.control_dt)
def default_pos_state(self):
print("Enter default pos state.")
print("Waiting for the Button A signal...")
while self.remote_controller.button[KeyMap.A] != 1:
for i in range(12):
motor_idx = self.config.joint2motor_idx[i]
self.low_cmd.motor_cmd[motor_idx].q = self.config.default_angles[i]
self.low_cmd.motor_cmd[motor_idx].dq = 0.0 # qd
self.low_cmd.motor_cmd[motor_idx].kp = 40.0
self.low_cmd.motor_cmd[motor_idx].kd = 0.6
self.low_cmd.motor_cmd[motor_idx].tau = 0
self.send_cmd(self.low_cmd)
time.sleep(self.config.control_dt)
def run(self):
self.counter += 1
for i in range(12):
self.qj[i] = self.low_state.motor_state[self.config.joint2motor_idx[i]].q
self.dqj[i] = self.low_state.motor_state[self.config.joint2motor_idx[i]].dq
ang_vel = np.array([self.low_state.imu_state.gyroscope], dtype=np.float32) * self.config.obs_scales_ang_vel
quat = self.low_state.imu_state.quaternion
gravity_orientation = get_gravity_orientation(quat) # imu_state quaternion: w, x, y, z
if self.use_remote_controller:
self.cmd[0] = self.remote_controller.ly
self.cmd[1] = self.remote_controller.lx * -1
self.cmd[2] = self.remote_controller.rx * -1
qj_obs = self.qj.copy()
qj_obs = (qj_obs - self.config.default_angles) * self.config.obs_scales_dof_pos
dqj_obs = self.dqj.copy()
dqj_obs = dqj_obs * self.config.obs_scales_dof_vel
self.obs[:3] = ang_vel
self.obs[3:6] = gravity_orientation
self.obs[6:9] = self.cmd * self.config.command_scale
self.obs[9:21] = qj_obs
self.obs[21:33] = dqj_obs
self.obs[33:45] = self.action
obs_tensor = torch.from_numpy(self.obs).unsqueeze(0)
self.action = self.policy(obs_tensor).detach().numpy().squeeze()
target_dof_pos = self.config.default_angles + self.action * self.config.action_scale
# target_dof_pos = self.config.default_angles
for i in range(12):
motor_idx = self.config.joint2motor_idx[i]
self.low_cmd.motor_cmd[motor_idx].q = target_dof_pos[i]
self.low_cmd.motor_cmd[motor_idx].dq = 0.0
self.low_cmd.motor_cmd[motor_idx].kp = 20.0
self.low_cmd.motor_cmd[motor_idx].kd = 0.5
self.low_cmd.motor_cmd[motor_idx].tau = 0
self.send_cmd(self.low_cmd)
time.sleep(self.config.control_dt)
# === 调试:遥控器 & 模型输出 ===
# print(f"RC: lx={self.remote_controller.lx:+.2f} ly={self.remote_controller.ly:+.2f} "
# f"rx={self.remote_controller.rx:+.2f}")
# print(f"OBS cmd: {self.obs[6:9]}") # 遥控器信号在 obs 的位置
# print(f"RAW action: {self.action[:4]}...") # 只看前 4 个,防止刷屏
# print(f"TARGET Q: {target_dof_pos[::3]}") # 每 3 个关节抽 1 个,易读
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("net", type=str, help="network interface")
args = parser.parse_args()
config_path = f"{LEGGED_GYM_ROOT_DIR}/deploy/deploy_real/configs/go2.yaml"
config = Config(config_path)
ChannelFactoryInitialize(0, args.net)
controller = Controller(config)
controller.zero_torque_state()
controller.move_to_default_pos()
controller.default_pos_state()
while True:
try:
controller.run()
if controller.remote_controller.button[KeyMap.select] == 1:
break
except KeyboardInterrupt:
break
create_damping_cmd(controller.low_cmd)
controller.send_cmd(controller.low_cmd)
print('Exit')

Binary file not shown.

119
doc/setup_en.md Normal file
View File

@@ -0,0 +1,119 @@
# Installation Guide
## System Requirements
- **Operating System**: Recommended Ubuntu 18.04 or later
- **GPU**: Nvidia GPU
- **Driver Version**: Recommended version 525 or later
---
## 1. Creating a Virtual Environment
It is recommended to run training or deployment programs in a virtual environment. Conda is recommended for creating virtual environments. If Conda is already installed on your system, you can skip step 1.1.
### 1.1 Download and Install MiniConda
MiniConda is a lightweight distribution of Conda, suitable for creating and managing virtual environments. Use the following commands to download and install:
```bash
mkdir -p ~/miniconda3
wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O ~/miniconda3/miniconda.sh
bash ~/miniconda3/miniconda.sh -b -u -p ~/miniconda3
rm ~/miniconda3/miniconda.sh
```
After installation, initialize Conda:
```bash
~/miniconda3/bin/conda init --all
source ~/.bashrc
```
### 1.2 Create a New Environment
Use the following command to create a virtual environment:
```bash
conda create -n unitree-rl python=3.8
```
### 1.3 Activate the Virtual Environment
```bash
conda activate unitree-rl
```
---
## 2. Installing Dependencies
### 2.1 Install PyTorch
PyTorch is a neural network computation framework used for model training and inference. Install it using the following command:
```bash
conda install pytorch==2.3.1 torchvision==0.18.1 torchaudio==2.3.1 pytorch-cuda=12.1 -c pytorch -c nvidia
```
### 2.2 Install Isaac Gym
Isaac Gym is a rigid body simulation and training framework provided by Nvidia.
#### 2.2.1 Download
Download [Isaac Gym](https://developer.nvidia.com/isaac-gym) from Nvidias official website.
#### 2.2.2 Install
After extracting the package, navigate to the `isaacgym/python` folder and install it using the following commands:
```bash
cd isaacgym/python
pip install -e .
```
#### 2.2.3 Verify Installation
Run the following command. If a window opens displaying 1080 balls falling, the installation was successful:
```bash
cd examples
python 1080_balls_of_solitude.py
```
If you encounter any issues, refer to the official documentation at `isaacgym/docs/index.html`.
### 2.3 Install rsl_rl
`rsl_rl` is a library implementing reinforcement learning algorithms.
#### 2.3.1 Install
```bash
cd rsl_rl
pip install -e .
```
### 2.4 Install go2_rl_gym
#### 2.4.1 Download
Clone the repository using Git:
```bash
git clone https://github.com/unitreerobotics/go2_rl_gym.git
```
#### 2.4.2 Install
Navigate to the directory and install it:
```bash
cd go2_rl_gym
pip install -e .
```
### 2.5 Install unitree_cpp_deploy (Optional)
Refer to our C++ deployment repository, which is based on unitree_rl_lab and specifically designed for deploying models trained in this repository: [unitree_cpp_deploy](https://github.com/wty-yy-mini/unitree_cpp_deploy).

137
doc/setup_zh.md Normal file
View File

@@ -0,0 +1,137 @@
# 安装配置文档
## 系统要求
- **操作系统**:推荐使用 Ubuntu 18.04 或更高版本
- **显卡**Nvidia 显卡
- **驱动版本**:建议使用 525 或更高版本
---
## 1. 创建虚拟环境
建议在虚拟环境中运行训练或部署程序,推荐使用 Conda 创建虚拟环境。如果您的系统中已经安装了 Conda可以跳过步骤 1.1。
### 1.1 下载并安装 MiniConda
MiniConda 是 Conda 的轻量级发行版,适用于创建和管理虚拟环境。使用以下命令下载并安装:
```bash
mkdir -p ~/miniconda3
wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O ~/miniconda3/miniconda.sh
bash ~/miniconda3/miniconda.sh -b -u -p ~/miniconda3
rm ~/miniconda3/miniconda.sh
```
安装完成后,初始化 Conda
```bash
~/miniconda3/bin/conda init --all
source ~/.bashrc
```
### 1.2 创建新环境
使用以下命令创建虚拟环境:
```bash
conda create -n unitree-rl python=3.8
```
### 1.3 激活虚拟环境
```bash
conda activate unitree-rl
```
---
## 2. 安装依赖
### 2.1 安装 PyTorch
PyTorch 是一个神经网络计算框架,用于模型训练和推理。使用以下命令安装:
```bash
conda install pytorch==2.3.1 torchvision==0.18.1 torchaudio==2.3.1 pytorch-cuda=12.1 -c pytorch -c nvidia
```
### 2.2 安装 Isaac Gym
Isaac Gym 是 Nvidia 提供的刚体仿真和训练框架。
#### 2.2.1 下载
从 Nvidia 官网下载 [Isaac Gym](https://developer.nvidia.com/isaac-gym)。
#### 2.2.2 安装
解压后进入 `isaacgym/python` 文件夹,执行以下命令安装:
```bash
cd isaacgym/python
pip install -e .
```
#### 2.2.3 验证安装
运行以下命令,若弹出窗口并显示 1080 个球下落,则安装成功:
```bash
cd examples
python 1080_balls_of_solitude.py
```
如有问题,可参考 `isaacgym/docs/index.html` 中的官方文档。
### 2.3 安装 rsl_rl
`rsl_rl` 是一个强化学习算法库。
#### 2.3.1 安装
```bash
cd rsl_rl
pip install -e .
```
### 2.4 安装 go2_rl_gym
#### 2.4.1 下载
通过 Git 克隆仓库:
```bash
git clone https://github.com/wty-yy/go2_rl_gym.git
```
#### 2.4.2 安装
进入目录并安装:
```bash
cd go2_rl_gym
pip install -e .
```
### 2.5 真机部署(可选)
#### 2.5.1 unitree_sdk2
C++ sdk, 编译参考[官方教程](https://github.com/unitreerobotics/unitree_sdk2?tab=readme-ov-file#environment-setup)
#### 2.5.2 unitree_sdk2_python选择用Python部署
```bash
conda create -n kaiwu python=3.8
conda activate kaiwu
pip3 install pytorch==2.3.1 torchvision==0.18.1 torchaudio==2.3.1 pytorch-cuda=12.1 -c pytorch -c nvidia
git clone https://github.com/unitreerobotics/unitree_sdk2_python.git
cd unitree_sdk2_python
pip install -e .
```
### 2.5.3 安装 unitree_cpp_deploy选择用C++部署)
我们基于unitree_rl_lab修改的C++部署仓库,专门用于部署本仓库训练的模型 [unitree_cpp_deploy](https://github.com/wty-yy-mini/unitree_cpp_deploy)

31
legged_gym/LICENSE Normal file
View File

@@ -0,0 +1,31 @@
Copyright (c) 2021, ETH Zurich, Nikita Rudin
Copyright (c) 2021, NVIDIA CORPORATION & AFFILIATES
All rights reserved.
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.
See licenses/assets for license information for assets included in this repository.
See licenses/dependencies for license information of dependencies of this package.

4
legged_gym/__init__.py Normal file
View File

@@ -0,0 +1,4 @@
import os
LEGGED_GYM_ROOT_DIR = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
LEGGED_GYM_ENVS_DIR = os.path.join(LEGGED_GYM_ROOT_DIR, 'legged_gym', 'envs')

View File

@@ -0,0 +1,11 @@
from legged_gym import LEGGED_GYM_ROOT_DIR, LEGGED_GYM_ENVS_DIR
from legged_gym.envs.go2.go2_env import Go2Robot
from legged_gym.envs.go2.go2_config import GO2Cfg, GO2CfgPPO, GO2CfgCTS, GO2CfgMoECTS
from .base.legged_robot import LeggedRobot
from legged_gym.utils.task_registry import task_registry
task_registry.register( "go2", Go2Robot, GO2Cfg(), GO2CfgPPO())
task_registry.register( "go2_cts", Go2Robot, GO2Cfg(), GO2CfgCTS())
task_registry.register( "go2_moe_cts", Go2Robot, GO2Cfg(), GO2CfgMoECTS())

View File

@@ -0,0 +1,25 @@
import inspect
class BaseConfig:
def __init__(self) -> None:
""" Initializes all member classes recursively. Ignores all namse starting with '__' (buit-in methods)."""
self.init_member_classes(self)
@staticmethod
def init_member_classes(obj):
# iterate over all attributes names
for key in dir(obj):
# disregard builtin attributes
# if key.startswith("__"):
if key=="__class__":
continue
# get the corresponding attribute object
var = getattr(obj, key)
# check if it the attribute is a class
if inspect.isclass(var):
# instantate the class
i_var = var()
# set the attribute to the instance instead of the type
setattr(obj, key, i_var)
# recursively init members of the attribute
BaseConfig.init_member_classes(i_var)

View File

@@ -0,0 +1,115 @@
import sys
from isaacgym import gymapi
from isaacgym import gymutil
import numpy as np
import torch
# Base class for RL tasks
class BaseTask():
def __init__(self, cfg, sim_params, physics_engine, sim_device, headless):
self.gym = gymapi.acquire_gym()
self.sim_params = sim_params
self.physics_engine = physics_engine
self.sim_device = sim_device
sim_device_type, self.sim_device_id = gymutil.parse_device_str(self.sim_device)
self.headless = headless
# env device is GPU only if sim is on GPU and use_gpu_pipeline=True, otherwise returned tensors are copied to CPU by physX.
if sim_device_type=='cuda' and sim_params.use_gpu_pipeline:
self.device = self.sim_device
else:
self.device = 'cpu'
# graphics device for rendering, -1 for no rendering
self.graphics_device_id = self.sim_device_id
if self.headless == True:
self.graphics_device_id = -1
self.num_envs = cfg.env.num_envs
self.num_obs = cfg.env.num_observations
self.num_privileged_obs = cfg.env.num_privileged_obs
self.num_actions = cfg.env.num_actions
# optimization flags for pytorch JIT
torch._C._jit_set_profiling_mode(False)
torch._C._jit_set_profiling_executor(False)
# allocate buffers
self.obs_buf = torch.zeros(self.num_envs, self.num_obs, device=self.device, dtype=torch.float)
self.rew_buf = torch.zeros(self.num_envs, device=self.device, dtype=torch.float)
self.reset_buf = torch.ones(self.num_envs, device=self.device, dtype=torch.long)
self.episode_length_buf = torch.zeros(self.num_envs, device=self.device, dtype=torch.long)
self.time_out_buf = torch.zeros(self.num_envs, device=self.device, dtype=torch.bool)
if self.num_privileged_obs is not None:
self.privileged_obs_buf = torch.zeros(self.num_envs, self.num_privileged_obs, device=self.device, dtype=torch.float)
else:
self.privileged_obs_buf = None
# self.num_privileged_obs = self.num_obs
self.extras = {}
# create envs, sim and viewer
self.create_sim()
self.gym.prepare_sim(self.sim)
# todo: read from config
self.enable_viewer_sync = True
self.viewer = None
# if running with a viewer, set up keyboard shortcuts and camera
if self.headless == False:
# subscribe to keyboard shortcuts
self.viewer = self.gym.create_viewer(
self.sim, gymapi.CameraProperties())
self.gym.subscribe_viewer_keyboard_event(
self.viewer, gymapi.KEY_ESCAPE, "QUIT")
self.gym.subscribe_viewer_keyboard_event(
self.viewer, gymapi.KEY_V, "toggle_viewer_sync")
def get_observations(self):
return self.obs_buf
def get_privileged_observations(self):
return self.privileged_obs_buf
def reset_idx(self, env_ids):
"""Reset selected robots"""
raise NotImplementedError
def reset(self):
""" Reset all robots"""
self.reset_idx(torch.arange(self.num_envs, device=self.device))
obs, privileged_obs, _, _, _ = self.step(torch.zeros(self.num_envs, self.num_actions, device=self.device, requires_grad=False))
return obs, privileged_obs
def step(self, actions):
raise NotImplementedError
def render(self, sync_frame_time=True):
if self.viewer:
# check for window closed
if self.gym.query_viewer_has_closed(self.viewer):
sys.exit()
# check for keyboard events
for evt in self.gym.query_viewer_action_events(self.viewer):
if evt.action == "QUIT" and evt.value > 0:
sys.exit()
elif evt.action == "toggle_viewer_sync" and evt.value > 0:
self.enable_viewer_sync = not self.enable_viewer_sync
# fetch results
if self.device != 'cpu':
self.gym.fetch_results(self.sim, True)
# step graphics
if self.enable_viewer_sync:
self.gym.step_graphics(self.sim)
self.gym.draw_viewer(self.viewer, self.sim, True)
if sync_frame_time:
self.gym.sync_frame_time(self.sim)
else:
self.gym.poll_viewer_events(self.viewer)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,361 @@
import math
from .base_config import BaseConfig
class LeggedRobotCfg(BaseConfig):
class env:
num_envs = 4096
num_observations = 48
num_privileged_obs = None # if not None a priviledge_obs_buf will be returned by step() (critic obs for assymetric training). None is returned otherwise
num_actions = 12
env_spacing = 3. # not used with heightfields/trimeshes
send_timeouts = True # send time out information to the algorithm
episode_length_s = 20 # episode length in seconds
test = False
class terrain:
mesh_type = 'trimesh' # none, plane, heightfield or trimesh
horizontal_scale = 0.1 # [m]
vertical_scale = 0.005 # [m]
border_size = 25 # [m]
curriculum = True
static_friction = 1.0
dynamic_friction = 1.0
restitution = 0.
# rough terrain only:
measure_heights = True
measured_points_x = [-0.8, -0.7, -0.6, -0.5, -0.4, -0.3, -0.2, -0.1, 0., 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8] # 1mx1.6m rectangle (without center line)
measured_points_y = [-0.5, -0.4, -0.3, -0.2, -0.1, 0., 0.1, 0.2, 0.3, 0.4, 0.5]
selected = False # select a unique terrain type and pass all arguments
terrain_kwargs = None # Dict of arguments for selected terrain
max_init_terrain_level = 5 # starting curriculum state
terrain_length = 8.
terrain_width = 8.
num_rows= 10 # number of terrain rows (levels)
num_cols = 20 # number of terrain cols (types)
terrain_spacing = 0.5 # spacing between different terrain types [m]
# [wave, slope, rough slope, stairs down, stairs up, obstacles, stepping stones, gap, flat]
terrain_proportions = [0.1, 0.1, 0.1, 0.2, 0.2, 0.1, 0.1, 0.1, 0.0]
# trimesh only:
slope_treshold = 0.75 # slopes above this threshold will be corrected to vertical surfaces
class commands:
curriculum = False
max_curriculum = 1.
num_commands = 4 # default: lin_vel_x, lin_vel_y, ang_vel_yaw, heading (in heading mode ang_vel_yaw is recomputed from heading error)
resampling_time = 10. # time before command are changed[s]
heading_command = False # if true: compute ang vel command from heading error
zero_command_curriculum = None
# start training with zero commands and then gradually increase zero command probability
# eg. {'start_iter': 0, 'end_iter': 1500, 'start_value': 0.0, 'end_value': 0.1}
limit_ang_vel_at_zero_command_prob = 0.0 # probability of add limiting angular velocity commands when zero command is sampled
limit_vel_prob = 0.0 # probability of limiting linear velocity command
limit_vel_invert_when_continuous = True # invert the limit logic when using continuous sample limit velocity commands
limit_vel = {"lin_vel_x": [-1, 1], "lin_vel_y": [-1, 1], "ang_vel_yaw": [-1, 0, 1]} # sample vel commands from min [-1] or zero [0] or max [1] range only
stop_heading_at_limit = True # stop heading updates when vel is limited
command_range_curriculum = [] # list for command range curriculums at specific training iterations
# eg: [{
# 'iter': 20000, # training iteration at which the command ranges are updated
# 'lin_vel_x': [-1.0, 1.0], # min max [m/s]
# 'lin_vel_y': [-1.0, 1.0], # min max [m/s]
# 'ang_vel_yaw': [-2.0, 2.0], # min max [rad/s]
# 'heading': [-1.57, 1.57], # min max [rad]
# }]
turn_over_zero_time = { # if turn_over is true, time robot must be stable before sampling new commands after a turn over
"backflip": 5.0,
"sideflip": 3.0,
}
# [wave, slope, rough slope, stairs up, stairs down, obstacles, stepping stones, gap, flat]
terrain_max_command_ranges = [
{'lin_vel_x': [-1.5, 1.5], 'lin_vel_y': [-1.5, 1.5], 'ang_vel_yaw': [-1.5, 1.5], 'heading': [-1.57, 1.57]}, # wave
{'lin_vel_x': [-1.5, 1.5], 'lin_vel_y': [-1.5, 1.5], 'ang_vel_yaw': [-1.5, 1.5], 'heading': [-1.57, 1.57]}, # slope
{'lin_vel_x': [-1.5, 1.5], 'lin_vel_y': [-1.5, 1.5], 'ang_vel_yaw': [-1.5, 1.5], 'heading': [-1.57, 1.57]}, # rough slope
{'lin_vel_x': [-1.0, 1.0], 'lin_vel_y': [-1.0, 1.0], 'ang_vel_yaw': [-1.5, 1.5], 'heading': [-1.57, 1.57]}, # stairs up
{'lin_vel_x': [-1.0, 1.0], 'lin_vel_y': [-1.0, 1.0], 'ang_vel_yaw': [-1.5, 1.5], 'heading': [-1.57, 1.57]}, # stairs down
{'lin_vel_x': [-1.0, 1.0], 'lin_vel_y': [-1.0, 1.0], 'ang_vel_yaw': [-1.5, 1.5], 'heading': [-1.57, 1.57]}, # obstacles
{'lin_vel_x': [-1.0, 1.0], 'lin_vel_y': [-1.0, 1.0], 'ang_vel_yaw': [-1.5, 1.5], 'heading': [-1.57, 1.57]}, # stepping stones
{'lin_vel_x': [-1.0, 1.0], 'lin_vel_y': [-1.0, 1.0], 'ang_vel_yaw': [-1.5, 1.5], 'heading': [-1.57, 1.57]}, # gap
{'lin_vel_x': [-2.0, 2.0], 'lin_vel_y': [-1.5, 1.5], 'ang_vel_yaw': [-1.5, 1.5], 'heading': [-1.57, 1.57]}, # flat
]
class ranges:
lin_vel_x = [-1.0, 1.0] # min max [m/s]
lin_vel_y = [-0.5, 0.5] # min max [m/s]
ang_vel_yaw = [-1, 1] # min max [rad/s]
heading = [-3.14, 3.14]
class init_state:
pos = [0.0, 0.0, 1.] # x,y,z [m]
rot = [0.0, 0.0, 0.0, 1.0] # x,y,z,w [quat]
lin_vel = [0.0, 0.0, 0.0] # x,y,z [m/s]
ang_vel = [0.0, 0.0, 0.0] # x,y,z [rad/s]
default_joint_angles = { # target angles when action = 0.0
"joint_a": 0.,
"joint_b": 0.}
turn_over = False # if true, initialize the robot in a flipped over position
turn_over_proportions = [0.0, 0.2, 0.8] # proportions for backflip, sideflip, no flip
turn_over_init_heights = { # initial heights range for each flip type
'backflip': [0.10, 0.15],
'sideflip': [0.16, 0.21],
}
class control:
control_type = 'P' # P: position, V: velocity, T: torques
# PD Drive parameters:
stiffness = {'joint_a': 10.0, 'joint_b': 15.} # [N*m/rad]
damping = {'joint_a': 1.0, 'joint_b': 1.5} # [N*m*s/rad]
# action scale: target angle = actionScale * action + defaultAngle
action_scale = 0.5
# decimation: Number of control action updates @ sim DT per policy DT
decimation = 4
class asset:
file = ""
name = "legged_robot" # actor name
foot_name = "None" # name of the feet bodies, used to index body state and contact force tensors
penalize_contacts_on = []
terminate_after_contacts_on = []
disable_gravity = False
collapse_fixed_joints = True # merge bodies connected by fixed joints. Specific fixed joints can be kept by adding " <... dont_collapse="true">
fix_base_link = False # fixe the base of the robot
default_dof_drive_mode = 3 # see GymDofDriveModeFlags (0 is none, 1 is pos tgt, 2 is vel tgt, 3 effort)
self_collisions = 0 # 1 to disable, 0 to enable...bitwise filter
replace_cylinder_with_capsule = True # replace collision cylinders with capsules, leads to faster/more stable simulation
flip_visual_attachments = True # Some .obj meshes must be flipped from y-up to z-up
density = 0.001
angular_damping = 0.
linear_damping = 0.
max_angular_velocity = 1000.
max_linear_velocity = 1000.
armature = 0.
thickness = 0.01
class domain_rand:
### Robot properties ###
robot_properties_update = None
# eg: {'start_iter': 5000, 'interval': 5000}
randomize_friction = True
friction_range = [0.2, 1.25]
randomize_base_mass = True
added_mass_range = [-1., 1.]
randomize_link_mass = True
multiplied_link_mass_range = [0.9, 1.1]
randomize_base_com = True
added_base_com_range = [-0.03, 0.03]
randomize_restitution = False # restitution to robot links (Robot init)
restitution_range = [0.0, 0.2]
### Environment reset ###
randomize_pd_gains = True
stiffness_multiplier_range = [0.9, 1.1]
damping_multiplier_range = [0.9, 1.1]
randomize_motor_zero_offset = True
motor_zero_offset_range = [-0.035, 0.035]
randomize_motor_strength = False # (Env reset)
motor_strength_range = [0.8, 1.2]
### Environment step ###
push_robots = True
push_interval_s = 4
max_push_vel_xy = 0.4
max_push_ang_vel = 0.6
randomize_action_delay = False # use last_action with 0~20 ms delay, 4 decimation
class rewards:
class scales:
termination = -0.0
tracking_lin_vel = 1.0
tracking_ang_vel = 0.5
lin_vel_z = -2.0
ang_vel_xy = -0.05
orientation = -0.
torques = -0.00001
dof_vel = -0.
dof_acc = -2.5e-7
base_height = -0.
feet_air_time = 1.0
collision = -1.
feet_stumble = -0.0
action_rate = -0.01
stand_still = -0.
class turn_over_scales:
upright = 1.0
only_positive_rewards = True # if true negative total rewards are clipped at zero (avoids early termination problems)
tracking_sigma = 0.25 # tracking reward = exp(-error^2/sigma)
soft_dof_pos_limit = 1. # percentage of urdf limits, values above this limit are penalized
soft_dof_vel_limit = 1.
soft_torque_limit = 1.
base_height_target = 1.
max_contact_force = 100. # forces above this value are penalized
curriculum_rewards = None # reward names to apply curriculum scaling to, List[dict]
# eg: [{'reward_name': 'lin_vel_z', 'start_iter': 0, 'end_iter': 1500, 'start_value': 1.0, 'end_value': 0.0}]
dynamic_sigma = None # linear interpolation of sigma based on command velocity, **Must start terrain curriculum first**
# eg: {
# "min_vel": 0.5, # min abs velocity to have default sigma
# "max_vel": 1.0, # max abs velocity to have max sigma
# # wave, slope, rough_slope, stairs up, stairs down, obstacles, stepping_stones, gap, flat]
# "max_sigma": [1/3, 1/4, 1/4, 1/2.7, 1/2.7, 1/2, 1, 1, 1/4]
# }
turn_over_roll_threshold = math.pi / 4 # threshold on roll to use turn over rewards
min_legs_distance = 0.1 # min distance between legs to not be considered stumbling
class normalization:
class obs_scales:
lin_vel = 2.0
ang_vel = 0.25
dof_pos = 1.0
dof_vel = 0.05
height_measurements = 2.5
clip_observations = 100.
clip_actions = 100.
class noise:
add_noise = True
noise_level = 1.0 # scales other values
class noise_scales:
dof_pos = 0.01
dof_vel = 1.5
lin_vel = 0.1
ang_vel = 0.2
gravity = 0.05
height_measurements = 0.1
# viewer camera:
class viewer:
ref_env = 0
pos = [10, 0, 6] # [m]
lookat = [11., 5, 3.] # [m]
class sim:
dt = 0.005
substeps = 1
gravity = [0., 0. ,-9.81] # [m/s^2]
up_axis = 1 # 0 is y, 1 is z
class physx:
num_threads = 10
solver_type = 1 # 0: pgs, 1: tgs
num_position_iterations = 4
num_velocity_iterations = 0
contact_offset = 0.01 # [m]
rest_offset = 0.0 # [m]
bounce_threshold_velocity = 0.5 #0.5 [m/s]
max_depenetration_velocity = 1.0
max_gpu_contact_pairs = 2**23 #2**24 -> needed for 8000 envs and more
default_buffer_size_multiplier = 5
contact_collection = 2 # 0: never, 1: last sub-step, 2: all sub-steps (default=2)
class LeggedRobotCfgPPO(BaseConfig):
seed = 1
runner_class_name = 'OnPolicyRunner'
class policy:
init_noise_std = 1.0
actor_hidden_dims = [512, 256, 128]
critic_hidden_dims = [512, 256, 128]
activation = 'elu' # can be elu, relu, selu, crelu, lrelu, tanh, sigmoid
# only for 'ActorCriticRecurrent':
# rnn_type = 'lstm'
# rnn_hidden_size = 512
# rnn_num_layers = 1
class algorithm:
# training params
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 # mini batch size = num_envs*nsteps / nminibatches
learning_rate = 1.e-3 #5.e-4
schedule = 'adaptive' # could be adaptive, fixed
gamma = 0.99
lam = 0.95
desired_kl = 0.01
max_grad_norm = 1.
class runner:
policy_class_name = 'ActorCritic'
algorithm_class_name = 'PPO'
num_steps_per_env = 24 # per iteration
max_iterations = 1500 # number of policy updates
# logging
save_interval = 50 # check for potential saves every this many iterations
experiment_name = 'test'
run_name = ''
# load and resume
resume = False
load_run = -1 # -1 = last run
checkpoint = -1 # -1 = last saved model
resume_path = None # updated from load_run and chkpt
class LeggedRobotCfgCTS(BaseConfig):
seed = 0
runner_class_name = "OnPolicyRunnerCTS"
history_length = 5
class policy:
init_noise_std = 1.0
actor_hidden_dims = [512, 256, 128]
critic_hidden_dims = [512, 256, 128]
teacher_encoder_hidden_dims = [512, 256]
student_encoder_hidden_dims = [512, 256]
activation = 'elu' # can be elu, relu, selu, crelu, lrelu, tanh, sigmoid
latent_dim = 32
norm_type = 'l2norm' # normalization type for encoders: l2norm, simnorm
class algorithm:
# training params
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 # mini batch size = num_envs*nsteps / nminibatches
learning_rate = 1.e-3 #5.e-4
student_encoder_learning_rate = 1e-3
schedule = 'adaptive' # could be adaptive, fixed
gamma = 0.99
lam = 0.95
desired_kl = 0.01
max_grad_norm = 1.
teacher_env_ratio = 0.75 # percentage of envs assigned to teacher
# teacher_env_ratio = 1.00 # percentage of envs assigned to teacher
class runner:
policy_class_name = 'ActorCriticCTS'
algorithm_class_name = 'CTS'
num_steps_per_env = 24 # per iteration
max_iterations = 1500 # number of policy updates
# logging
save_interval = 50 # check for potential saves every this many iterations
experiment_name = 'test'
run_name = ''
# load and resume
resume = False
load_run = -1 # -1 = last run
checkpoint = -1 # -1 = last saved model
resume_path = None # updated from load_run and chkpt
class LeggedRobotCfgMoECTS(LeggedRobotCfgCTS):
class policy(LeggedRobotCfgCTS.policy):
obs_no_goal_mask = None # mask for observation without goal inputs
student_expert_num = 8 # number of experts in the student model
class algorithm(LeggedRobotCfgCTS.algorithm):
load_balance_coef = 0.01 # coefficient for load balance loss
class runner(LeggedRobotCfgCTS.runner):
policy_class_name = 'ActorCriticMoECTS'
algorithm_class_name = 'MoECTS'

View File

@@ -0,0 +1,266 @@
import math
from legged_gym.envs.base.legged_robot_config import LeggedRobotCfg, LeggedRobotCfgPPO, LeggedRobotCfgCTS, LeggedRobotCfgMoECTS, LeggedRobotCfgMoECTS
class GO2Cfg(LeggedRobotCfg):
class init_state(LeggedRobotCfg.init_state):
pos = [0.0, 0.0, 0.42] # x,y,z [m]
default_joint_angles = { # = target angles [rad] when action = 0.0
'FL_hip_joint': 0.1, # [rad]
'RL_hip_joint': 0.1, # [rad]
'FR_hip_joint': -0.1 , # [rad]
'RR_hip_joint': -0.1, # [rad]
'FL_thigh_joint': 0.8, # [rad]
'RL_thigh_joint': 1., # [rad]
'FR_thigh_joint': 0.8, # [rad]
'RR_thigh_joint': 1., # [rad]
'FL_calf_joint': -1.5, # [rad]
'RL_calf_joint': -1.5, # [rad]
'FR_calf_joint': -1.5, # [rad]
'RR_calf_joint': -1.5, # [rad]
}
turn_over = False # initialize the robot in a flipped over position
# turn_over_proportions = [0.1, 0.3, 0.6] # proportions for backflip, sideflip, noflip
turn_over_proportions = [0.0, 0.2, 0.8] # proportions for backflip, sideflip, noflip
turn_over_init_heights = { # initial heights range for each flip type
'backflip': [0.10, 0.15],
'sideflip': [0.16, 0.21],
}
# turn_over_proportions = [0.0, 1.0, 0.0] # proportions for backflip, sideflip, noflip
class env(LeggedRobotCfg.env):
num_envs = 8192
num_observations = 45
# obs(45) + base_lin_vel(3) + height_measurements(187)
num_privileged_obs = 45 + 3 + 4 + 12 + 12 + 187 # 263
# num_privileged_obs = 45 + 3 + 187 # 235
# num_privileged_obs = 48 # without height measurements
episode_length_s = 25
class domain_rand(LeggedRobotCfg.domain_rand):
### Robot properties ###
randomize_friction = True
friction_range = [0.0, 2.0]
randomize_base_mass = True
added_mass_range = [-1., 1.]
randomize_link_mass = True
multiplied_link_mass_range = [0.9, 1.1]
randomize_base_com = True
added_base_com_range = [-0.03, 0.03]
randomize_restitution = True # restitution to robot links (Robot init)
restitution_range = [0.0, 0.5]
### Environment reset ###
randomize_pd_gains = True
stiffness_multiplier_range = [0.9, 1.1]
damping_multiplier_range = [0.9, 1.1]
randomize_motor_zero_offset = True
motor_zero_offset_range = [-0.035, 0.035]
randomize_motor_strength = True # (Env reset)
motor_strength_range = [0.8, 1.2]
### Environment step ###
push_robots = True
push_interval_s = 4
max_push_vel_xy = 0.4
max_push_ang_vel = 0.6
randomize_action_delay = True # use last_action with 0~20 ms delay, 4 decimation
class control(LeggedRobotCfg.control):
# PD Drive parameters:
control_type = 'P'
stiffness = {'joint': 20.0} # [N*m/rad]
damping = {'joint': 0.5} # [N*m*s/rad]
# action scale: target angle = actionScale * action + defaultAngle
action_scale = 0.25
# decimation: Number of control action updates @ sim DT per policy DT
decimation = 4
class terrain(LeggedRobotCfg.terrain):
max_init_terrain_level = 5
# [wave, slope, rough_slope, stairs up, stairs down, obstacles, stepping_stones, gap, flat]
# terrain_proportions = [0.2, 0.05, 0.05, 0.30, 0.05, 0.25, 0.0, 0.0, 0.1] # 更偏向wave
terrain_proportions = [0.05, 0.20, 0.05, 0.25, 0.10, 0.20, 0.0, 0.0, 0.15] # 这个更偏向平地斜坡
# terrain_proportions = [0.20, 0.05, 0.05, 0.30, 0.15, 0.20, 0.0, 0.0, 0.05] # 更偏向wave和stairs
# terrain_proportions = [0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0]
# terrain_proportions = [0.3, 0.3, 0.3, 0.0, 0.0, 0.0, 0.0, 0.0, 0.1]
# terrain_proportions = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0]
class commands(LeggedRobotCfg.commands):
curriculum = False
max_curriculum = 1.
num_commands = 4 # default: lin_vel_x, lin_vel_y, ang_vel_yaw (in heading mode ang_vel_yaw is recomputed from heading error)
resampling_time = 5. # time before command are changed[s]
heading_command = False # if true: compute ang vel command from heading error
# start training with zero commands and then gradually increase zero command probability
zero_command_curriculum = {'start_iter': 0, 'end_iter': 1500, 'start_value': 0.0, 'end_value': 0.1}
limit_ang_vel_at_zero_command_prob = 0.2 # probability of add limiting angular velocity commands when zero command is sampled
limit_vel_prob = 0.2 # probability of limiting linear velocity command
limit_vel_invert_when_continuous = True # invert the limit logic when using continuous sample limit velocity commands
limit_vel = {"lin_vel_x": [-1, 1], "lin_vel_y": [-1, 1], "ang_vel_yaw": [-1, 0, 1]} # sample vel commands from min [-1] or zero [0] or max [1] range only
stop_heading_at_limit = True # stop heading updates when vel is limited
command_range_curriculum = [{ # list for command range curriculums at specific training iterations
'iter': 20000, # training iteration at which the command ranges are updated
'lin_vel_x': [-1.0, 1.0], # min max [m/s]
'lin_vel_y': [-1.0, 1.0], # min max [m/s]
'ang_vel_yaw': [-1.5, 1.5], # min max [rad/s]
'heading': [-1.57, 1.57], # min max [rad]
}, { # list for command range curriculums at specific training iterations
'iter': 50000, # training iteration at which the command ranges are updated
'lin_vel_x': [-2.0, 2.0], # min max [m/s]
'lin_vel_y': [-1.0, 1.0], # min max [m/s]
'ang_vel_yaw': [-2.0, 2.0], # min max [rad/s]
'heading': [-1.57, 1.57], # min max [rad]
}]
turn_over_zero_time = { # if turn_over is true, time robot must be stable before sampling new commands after a turn over
"backflip": 5.0,
"sideflip": 3.0,
}
# [wave, slope, rough slope, stairs up, stairs down, obstacles, stepping stones, gap, flat]
terrain_max_command_ranges = [
{'lin_vel_x': [-1.5, 1.5], 'lin_vel_y': [-1.0, 1.0], 'ang_vel_yaw': [-1.5, 1.5], 'heading': [-1.57, 1.57]}, # wave
{'lin_vel_x': [-1.5, 1.5], 'lin_vel_y': [-1.0, 1.0], 'ang_vel_yaw': [-1.5, 1.5], 'heading': [-1.57, 1.57]}, # slope
{'lin_vel_x': [-1.5, 1.5], 'lin_vel_y': [-1.0, 1.0], 'ang_vel_yaw': [-1.5, 1.5], 'heading': [-1.57, 1.57]}, # rough slope
{'lin_vel_x': [-1.0, 1.0], 'lin_vel_y': [-1.0, 1.0], 'ang_vel_yaw': [-1.5, 1.5], 'heading': [-1.57, 1.57]}, # stairs up
{'lin_vel_x': [-1.0, 1.0], 'lin_vel_y': [-1.0, 1.0], 'ang_vel_yaw': [-1.5, 1.5], 'heading': [-1.57, 1.57]}, # stairs down
{'lin_vel_x': [-1.0, 1.0], 'lin_vel_y': [-1.0, 1.0], 'ang_vel_yaw': [-1.5, 1.5], 'heading': [-1.57, 1.57]}, # obstacles
{'lin_vel_x': [-1.0, 1.0], 'lin_vel_y': [-1.0, 1.0], 'ang_vel_yaw': [-1.5, 1.5], 'heading': [-1.57, 1.57]}, # stepping stones
{'lin_vel_x': [-1.0, 1.0], 'lin_vel_y': [-1.0, 1.0], 'ang_vel_yaw': [-1.5, 1.5], 'heading': [-1.57, 1.57]}, # gap
{'lin_vel_x': [-2.0, 2.0], 'lin_vel_y': [-1.0, 1.0], 'ang_vel_yaw': [-2.0, 2.0], 'heading': [-1.57, 1.57]}, # flat
]
class ranges:
lin_vel_x = [-0.5, 0.5] # min max [m/s]
lin_vel_y = [-0.5, 0.5] # min max [m/s]
ang_vel_yaw = [-1.0, 1.0] # min max [rad/s]
heading = [-1.57, 1.57] # min max [rad]
class asset(LeggedRobotCfg.asset):
file = '{LEGGED_GYM_ROOT_DIR}/resources/robots/go2/urdf/go2.urdf'
name = "go2"
foot_name = "foot"
penalize_contacts_on = ["thigh", "calf"]
terminate_after_contacts_on = ["base"]
self_collisions = 1 # 1 to disable, 0 to enable...bitwise filter
class rewards(LeggedRobotCfg.rewards):
soft_dof_pos_limit = 0.9
base_height_target = 0.38
only_positive_rewards = False
max_contact_force = 147. # forces above this value are penalized, go2 weight 15kg
curriculum_rewards = [
{'reward_name': 'lin_vel_z', 'start_iter': 0, 'end_iter': 1500, 'start_value': 1.0, 'end_value': 0.0},
{'reward_name': 'correct_base_height', 'start_iter': 0, 'end_iter': 5000, 'start_value': 1.0, 'end_value': 10.0},
# {'reward_name': 'dof_power', 'start_iter': 0, 'end_iter': 3000, 'start_value': 1.0, 'end_value': 0.1},
# {'reward_name': 'upright', 'start_iter': 0, 'end_iter': 1500, 'start_value': 1.0, 'end_value': 0.0},
]
tracking_sigma = 0.25 # tracking reward = exp(-error^2/sigma)
dynamic_sigma = { # linear interpolation of sigma based on command velocity, **Must start terrain curriculum first**
"min_lin_vel": 0.5, # min abs linear velocity to have default sigma
"max_lin_vel": 1.5, # max abs linear velocity to have max sigma
"min_ang_vel": 1.0, # min abs angular velocity to have default sigma
"max_ang_vel": 2.0, # max abs angular velocity to have max sigma
# wave, slope, rough_slope, stairs up, stairs down, obstacles, stepping_stones, gap, flat]
# "max_sigma": [1/3, 1/4, 1/4, 1/2.7, 1/2.7, 1/2, 1, 1, 1/4]
"max_sigma": [5/12, 1/4, 1/4, 1/2, 1/2, 3/4, 1, 1, 1/4]
}
min_legs_distance = 0.1 # min distance between legs to not be considered stumbling
class scales:
# tracking_lin_vel = 1.0
# tracking_ang_vel = 0.2
# lin_vel_z = -10.0
# base_height = -50.0
# action_rate = -0.005
# similar_to_default = -0.1
# dof_power = -1e-3 # 能够明显抑制跳跃
# dof_acc = -3e-7
# tracking_lin_vel = 1.0
# tracking_ang_vel = 0.5
# lin_vel_z = -2.0
# ang_vel_xy = -0.05
# dof_acc = -2.5e-7
# dof_power = -1e-3 # 能够明显抑制跳跃
# # torques = -1e-4 # 无用会走着走着倒了
# correct_base_height = -10.0
# action_rate = -0.01
# action_smoothness = -0.01
# collision = -1.0
# dof_pos_limits = -2.0
# feet_regulation = -0.05
# hip_to_default = -0.1
# similar_to_default = -0.05
# CTS reward
tracking_lin_vel = 1.0
tracking_ang_vel = 0.5
lin_vel_z = -2.0
ang_vel_xy = -0.05
dof_acc = -2.5e-7
dof_power = -2e-5
torques = -1e-4
correct_base_height = -1.0
action_rate = -0.01
action_smoothness = -0.01
collision = -1.0
dof_pos_limits = -2.0
feet_regulation = -0.05
# CTS奖励训出来双脚距离非常近, 真机效果很差, 但是sim2sim能上20cm楼梯, 尝试加入hip_to_default奖励或similar_to_default奖励
hip_to_default = -0.05 # 在训练到y=1.5时, 双脚会明显碰撞, 为避免该问题提升hip, 效果更差, 还是保持0.05 (y最大也只到0.1了)
# legs_distance = -1.5 # 奖励双脚距离, 避免CTS训练出来双脚距离过近, 尝试加入后robogauge flat验证效果变差, 删除
# similar_to_default = -0.01
# feet_contact_forces = -1.0 # 尝试加入但并没有起到任何效果, 删除
turn_over_roll_threshold = math.pi / 4 # threshold on roll to use turn over rewards
class turn_over_scales:
upright = 1.0
# dof_acc = -2.5e-7
# dof_power = -2e-5
# action_rate = -0.001
# action_smoothness = -0.001
class noise(LeggedRobotCfg.noise):
add_noise = True
class GO2CfgPPO(LeggedRobotCfgPPO):
class algorithm(LeggedRobotCfgPPO.algorithm):
entropy_coef = 0.01
class runner(LeggedRobotCfgPPO.runner):
run_name = ''
experiment_name = 'go2_ppo'
max_iterations = 100000
save_interval = 500
class GO2CfgCTS(LeggedRobotCfgCTS):
class runner(LeggedRobotCfgCTS.runner):
num_steps_per_env = 24
run_name = ''
experiment_name = 'go2_cts'
max_iterations = 100000
save_interval = 500
class policy(LeggedRobotCfgCTS.policy):
latent_dim = 32
norm_type = 'l2norm'
class GO2CfgMoECTS(LeggedRobotCfgMoECTS):
class policy(LeggedRobotCfgMoECTS.policy):
obs_no_goal_mask = [True] * 6 + [False] * 3 + [True] * 36 # mask for obs without command info
student_expert_num = 8 # number of experts in the student model
class algorithm(LeggedRobotCfgMoECTS.algorithm):
load_balance_coef = 0.01
class runner(LeggedRobotCfgMoECTS.runner):
run_name = ''
experiment_name = 'go2_moe_cts'
max_iterations = 100000
save_interval = 500

View File

@@ -0,0 +1,53 @@
from legged_gym.envs.base.legged_robot import LeggedRobot
from isaacgym.torch_utils import *
from isaacgym import gymtorch, gymapi, gymutil
import torch
class Go2Robot(LeggedRobot):
def _get_noise_scale_vec(self, cfg):
noise_vec = torch.zeros_like(self.obs_buf[0])
self.add_noise = self.cfg.noise.add_noise
noise_scales = self.cfg.noise.noise_scales
noise_level = self.cfg.noise.noise_level
noise_vec[:3] = noise_scales.ang_vel * noise_level * self.obs_scales.ang_vel
noise_vec[3:6] = noise_scales.gravity * noise_level
noise_vec[6:9] = 0. # commands
noise_vec[9:9+self.num_actions] = noise_scales.dof_pos * noise_level * self.obs_scales.dof_pos
noise_vec[9+self.num_actions:9+2*self.num_actions] = noise_scales.dof_vel * noise_level * self.obs_scales.dof_vel
noise_vec[9+2*self.num_actions:9+3*self.num_actions] = 0. # previous actions
return noise_vec
def compute_observations(self):
""" Computes observations
"""
self.obs_buf = torch.cat((self.base_ang_vel * self.obs_scales.ang_vel,
self.projected_gravity,
self.commands[:, :3] * self.commands_scale,
(self.dof_pos - self.default_dof_pos) * self.obs_scales.dof_pos,
self.dof_vel * self.obs_scales.dof_vel,
self.actions,
),dim=-1)
heights = torch.clip(self.root_states[:, 2].unsqueeze(1) - 0.5 - self.measured_heights, -1, 1.0) * self.obs_scales.height_measurements
self.privileged_obs_buf = torch.cat(( self.base_lin_vel * self.obs_scales.lin_vel,
self.base_ang_vel * self.obs_scales.ang_vel,
self.projected_gravity,
self.commands[:, :3] * self.commands_scale,
(self.dof_pos - self.default_dof_pos) * self.obs_scales.dof_pos,
self.dof_vel * self.obs_scales.dof_vel,
self.actions,
torch.norm(self.contact_forces[:, self.feet_indices, :], dim=-1) * 1e-3, # foot contact forces (4,)
self.torques / self.torque_limits, # motor torques (12,)
(self.last_dof_vel - self.dof_vel) / self.dt * 1e-4, # motor accelerations (12,)
heights, # height measurements (187,)
),dim=-1)
# print(f"foot contact: {self.privileged_obs_buf[:,48:48+4].min(), self.privileged_obs_buf[:,48:48+4].max()}")
# print(f"torques: {self.privileged_obs_buf[:,48+4:48+4+12].min(), self.privileged_obs_buf[:,48+4:48+4+12].max()}")
# print(f"acc: {self.privileged_obs_buf[:,48+4+12:48+4+12+12].min(), self.privileged_obs_buf[:,48+4+12:48+4+12+12].max()}")
if self.add_noise:
self.obs_buf += (2 * torch.rand_like(self.obs_buf) - 1) * self.noise_scale_vec

View File

@@ -0,0 +1,70 @@
import sys
from legged_gym import LEGGED_GYM_ROOT_DIR
import os
import sys
from legged_gym import LEGGED_GYM_ROOT_DIR
import isaacgym
from legged_gym.envs import *
from legged_gym.utils import get_args, task_registry, Logger
from legged_gym.utils.exporter import export_policy_as_jit, export_policy_as_onnx, export_policy_as_pkl
import numpy as np
import torch
def play(args):
env_cfg, train_cfg = task_registry.get_cfgs(name=args.task)
# override some parameters for testing
env_cfg.env.num_envs = min(env_cfg.env.num_envs, 100)
# env_cfg.terrain.mesh_type = 'plane'
env_cfg.terrain.num_rows = 7
env_cfg.terrain.num_cols = 7
env_cfg.terrain.curriculum = False
env_cfg.noise.add_noise = False
env_cfg.domain_rand.randomize_friction = False
env_cfg.domain_rand.push_robots = False
env_cfg.domain_rand.randomize_base_mass = False
env_cfg.domain_rand.randomize_link_mass = False
env_cfg.domain_rand.randomize_base_com = False
env_cfg.domain_rand.randomize_pd_gains = False
env_cfg.domain_rand.randomize_motor_zero_offset = False
env_cfg.env.test = True
# prepare environment
env, _ = task_registry.make_env(name=args.task, args=args, env_cfg=env_cfg)
obs = env.get_observations()
# load policy
train_cfg.runner.resume = True
runner, train_cfg = task_registry.make_alg_runner(env=env, name=args.task, args=args, train_cfg=train_cfg)
policy = runner.get_inference_policy(device=env.device)
# export policy as a jit module (used to run it from C++)
if EXPORT_POLICY:
path = os.path.join(LEGGED_GYM_ROOT_DIR, 'logs', train_cfg.runner.experiment_name, 'exported', 'policies')
if hasattr(runner.alg, 'actor_critic'):
model = runner.alg.actor_critic
else:
model = runner.alg.model
export_policy_as_jit(model, path)
export_policy_as_onnx(model, path)
export_policy_as_pkl(model, path)
print('Exported policy as jit script / onnx to: ', path)
for i in range(10*int(env.max_episode_length)):
actions = policy(obs.detach())
if FIX_COMMAND:
env.commands[:, 0] = 1.0
env.commands[:, 1] = 0.0
env.commands[:, 2] = 0.0
obs, _, rews, dones, infos = env.step(actions.detach())
if __name__ == '__main__':
EXPORT_POLICY = True
RECORD_FRAMES = False
MOVE_CAMERA = False
FIX_COMMAND = True
args = get_args()
play(args)

View File

@@ -0,0 +1,20 @@
import os
import numpy as np
from datetime import datetime
import sys
import isaacgym
from legged_gym.envs import *
from legged_gym.utils import get_args, task_registry
import torch
def train(args):
env, env_cfg = task_registry.make_env(name=args.task, args=args)
runner, train_cfg = task_registry.make_alg_runner(env=env, name=args.task, args=args)
env.common_step_counter = runner.current_learning_iteration * env.num_steps_per_env # resume env step counter
env.update_reward_curriculum(force_update=True) # force update reward curriculum at start
runner.learn(num_learning_iterations=train_cfg.runner.max_iterations, init_at_random_ep_len=True)
if __name__ == '__main__':
args = get_args()
train(args)

View File

@@ -0,0 +1,5 @@
from .helpers import class_to_dict, get_load_path, get_args, set_seed, update_class_from_dict
from .task_registry import task_registry
from .logger import Logger
from .math import *
from .terrain import Terrain

View File

@@ -0,0 +1,268 @@
# Copyright (c) 2022-2025, The Isaac Lab Project Developers.
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
import copy
import os
import torch
from torch import nn
from typing import Optional
def export_policy_as_jit(policy: object, path: str, normalizer: Optional[object] = None, filename="policy.pt"):
"""Export policy into a Torch JIT file.
Args:
policy: The policy torch module.
normalizer: The empirical normalizer module. If None, Identity is used.
path: The path to the saving directory.
filename: The name of exported JIT file. Defaults to "policy.pt".
"""
policy_exporter = _TorchPolicyExporter(policy, normalizer)
policy_exporter.export(path, filename)
def export_policy_as_onnx(
policy: object, path: str, normalizer: Optional[object] = None, filename="policy.onnx", verbose=False
):
"""Export policy into a Torch ONNX file.
Args:
policy: The policy torch module.
normalizer: The empirical normalizer module. If None, Identity is used.
path: The path to the saving directory.
filename: The name of exported ONNX file. Defaults to "policy.onnx".
verbose: Whether to print the model summary. Defaults to False.
"""
if not os.path.exists(path):
os.makedirs(path, exist_ok=True)
policy_exporter = _OnnxPolicyExporter(policy, normalizer, verbose)
policy_exporter.export(path, filename)
def export_policy_as_pkl(
policy: nn.Module, path: str, filename="policy.pkl"
):
"""Export policy into a Torch pkl file.
Args:
policy: The policy torch module.
normalizer: The empirical normalizer module. If None, Identity is used.
path: The path to the saving directory.
filename: The name of exported pkl file. Defaults to "policy.pkl".
"""
if not os.path.exists(path):
os.makedirs(path, exist_ok=True)
model_dict = policy.state_dict()
torch.save(model_dict, os.path.join(path, filename))
"""
Helper Classes - Private.
"""
class _TorchPolicyExporter(torch.nn.Module):
"""Exporter of actor-critic into JIT file."""
def __init__(self, policy, normalizer=None):
super().__init__()
self.is_recurrent = policy.is_recurrent
# copy policy parameters
if hasattr(policy, "student_encoder"):
self.student_encoder = copy.deepcopy(policy.student_encoder).cpu()
self.history = torch.zeros([1, policy.history.shape[1], policy.history.shape[2]], device='cpu')
self.forward = self.forward_cts
if hasattr(policy, "student_moe_encoder"):
self.student_moe_encoder = copy.deepcopy(policy.student_moe_encoder).cpu()
self.obs_no_goal_mask = copy.deepcopy(policy.obs_no_goal_mask).cpu()
self.history_length = policy.history.shape[1]
self.history = torch.zeros([1, policy.history.shape[1], policy.history.shape[2]], device='cpu')
self.forward = self.forward_moe_cts
if hasattr(policy, "actor"):
self.actor = copy.deepcopy(policy.actor)
if self.is_recurrent:
self.rnn = copy.deepcopy(policy.memory_a.rnn)
elif hasattr(policy, "student"):
self.actor = copy.deepcopy(policy.student)
if self.is_recurrent:
self.rnn = copy.deepcopy(policy.memory_s.rnn)
else:
raise ValueError("Policy does not have an actor/student module.")
# set up recurrent network
if self.is_recurrent:
self.rnn.cpu()
self.register_buffer("hidden_state", torch.zeros(self.rnn.num_layers, 1, self.rnn.hidden_size))
self.register_buffer("cell_state", torch.zeros(self.rnn.num_layers, 1, self.rnn.hidden_size))
self.forward = self.forward_lstm
self.reset = self.reset_memory
# copy normalizer if exists
if normalizer:
self.normalizer = copy.deepcopy(normalizer)
else:
self.normalizer = torch.nn.Identity()
def forward_lstm(self, x):
x = self.normalizer(x)
x, (h, c) = self.rnn(x.unsqueeze(0), (self.hidden_state, self.cell_state))
self.hidden_state[:] = h
self.cell_state[:] = c
x = x.squeeze(0)
return self.actor(x)
def forward(self, x):
return self.actor(self.normalizer(x))
def forward_cts(self, x): # x is single observations
x = self.normalizer(x)
self.history = torch.cat([self.history[:, 1:], x.unsqueeze(1)], dim=1)
latent = self.student_encoder(self.history.flatten(1))
x = torch.cat([latent, x], dim=1)
return self.actor(x)
def forward_moe_cts(self, x): # x is single observations
x = self.normalizer(x)
self.history = torch.cat([self.history[:, 1:], x.unsqueeze(1)], dim=1)
history_no_goal = self.history.reshape(1, self.history_length, -1)[:, :, self.obs_no_goal_mask].reshape(1, -1)
latent, weights = self.student_moe_encoder(self.history.flatten(1), history_no_goal)
x = torch.cat([latent, x], dim=1)
return self.actor(x), (weights, latent)
@torch.jit.export
def reset(self):
if hasattr(self, 'history'):
self.history = torch.zeros_like(self.history)
def reset_memory(self):
self.hidden_state[:] = 0.0
self.cell_state[:] = 0.0
def export(self, path, filename):
os.makedirs(path, exist_ok=True)
path = os.path.join(path, filename)
self.to("cpu")
traced_script_module = torch.jit.script(self)
traced_script_module.save(path)
class _OnnxPolicyExporter(torch.nn.Module):
"""Exporter of actor-critic into ONNX file."""
def __init__(self, policy, normalizer=None, verbose=False):
super().__init__()
self.verbose = verbose
self.input_dim = None
self.num_actions = 12
self.normalizer = torch.nn.Identity()
# copy policy parameters
if hasattr(policy, 'student_encoder'):
self.student_encoder = copy.deepcopy(policy.student_encoder)
self.forward = self.forward_cts
self.input_dim = self.student_encoder[0].in_features
elif hasattr(policy, "student_moe_encoder"):
self.student_moe_encoder = copy.deepcopy(policy.student_moe_encoder)
self.obs_no_goal_mask = copy.deepcopy(policy.obs_no_goal_mask).cpu()
self.history_length = policy.history.shape[1]
self.forward = self.forward_moe_cts
self.input_dim = self.history_length * policy.history.shape[2]
if hasattr(policy, "actor"):
self.actor = copy.deepcopy(policy.actor)
if hasattr(self, 'is_recurrent') and self.is_recurrent:
self.rnn = copy.deepcopy(policy.memory_a.rnn)
if self.input_dim is None:
self.input_dim = self.actor[0].in_features
else:
raise ValueError("Policy does not have an actor/student module.")
def forward_cts(self, x): # x is stack observations by terms
x = self.normalizer(x)
term_dims = [3, 3, 3, self.num_actions, self.num_actions, self.num_actions]
obs_dim = sum(term_dims)
if x.shape[1] % obs_dim != 0:
raise ValueError(f"x.shape[1] ({x.shape[1]}) 不是 obs_dim ({obs_dim}) 的整数倍")
frames = x.shape[1] // obs_dim
split_sizes = [dim * frames for dim in term_dims]
# [B, dim0*frames], [B, dim1*frames], ...
term_chunks = torch.split(x, split_sizes, dim=1)
# [ [B, frames, dim0], [B, frames, dim1], ... ]
frame_terms_reshaped = [
chunk.view(-1, frames, dim)
for chunk, dim in zip(term_chunks, term_dims)
]
history_by_frame = []
for i in range(frames):
# [ [B, dim0], [B, dim1], ... ]
terms_for_this_frame = [ftr[:, i, :] for ftr in frame_terms_reshaped]
history_by_frame.append(torch.cat(terms_for_this_frame, dim=1))
# [B, (Frame0_AllTerms), (Frame1_AllTerms), ...]
history = torch.cat(history_by_frame, dim=1)
last_obs = history[:, -obs_dim:]
latent = self.student_encoder(history)
x = torch.cat([latent, last_obs], dim=1)
return self.actor(x)
def forward_moe_cts(self, x):
x = self.normalizer(x)
term_dims = [3, 3, 3, self.num_actions, self.num_actions, self.num_actions]
obs_dim = sum(term_dims)
if x.shape[1] % obs_dim != 0:
raise ValueError(f"x.shape[1] ({x.shape[1]}) 不是 obs_dim ({obs_dim}) 的整数倍")
frames = x.shape[1] // obs_dim
split_sizes = [dim * frames for dim in term_dims]
# [B, dim0*frames], [B, dim1*frames], ...
term_chunks = torch.split(x, split_sizes, dim=1)
# [ [B, frames, dim0], [B, frames, dim1], ... ]
frame_terms_reshaped = [
chunk.view(-1, frames, dim)
for chunk, dim in zip(term_chunks, term_dims)
]
history_by_frame = []
for i in range(frames):
# [ [B, dim0], [B, dim1], ... ]
terms_for_this_frame = [ftr[:, i, :] for ftr in frame_terms_reshaped]
history_by_frame.append(torch.cat(terms_for_this_frame, dim=1))
# [B, (Frame0_AllTerms), (Frame1_AllTerms), ...]
history = torch.cat(history_by_frame, dim=1)
last_obs = history[:, -obs_dim:]
history_3d = history.view(-1, self.history_length, obs_dim)
history_no_goal = history_3d[:, :, self.obs_no_goal_mask].reshape(x.shape[0], -1)
latent, weights = self.student_moe_encoder(history, history_no_goal)
x = torch.cat([latent, last_obs], dim=1)
return self.actor(x), weights, latent
def export(self, path, filename):
self.to("cpu")
obs = torch.zeros(1, self.input_dim)
output_names = ["actions"]
if self.forward == self.forward_moe_cts:
output_names.append("weights")
output_names.append("latent")
torch.onnx.export(
self,
obs,
os.path.join(path, filename),
export_params=True,
opset_version=11,
verbose=self.verbose,
input_names=["obs"],
output_names=output_names,
dynamic_axes={},
)

193
legged_gym/utils/helpers.py Normal file
View File

@@ -0,0 +1,193 @@
import os
import copy
import torch
import numpy as np
import random
from pathlib import Path
from isaacgym import gymapi
from isaacgym import gymutil
from legged_gym import LEGGED_GYM_ROOT_DIR, LEGGED_GYM_ENVS_DIR
def class_to_dict(obj) -> dict:
if not hasattr(obj,"__dict__"):
return obj
result = {}
for key in dir(obj):
if key.startswith("_"):
continue
element = []
val = getattr(obj, key)
if isinstance(val, list):
for item in val:
element.append(class_to_dict(item))
else:
element = class_to_dict(val)
result[key] = element
return result
def update_class_from_dict(obj, dict):
for key, val in dict.items():
attr = getattr(obj, key, None)
if isinstance(attr, type):
update_class_from_dict(attr, val)
else:
setattr(obj, key, val)
return
def set_seed(seed):
if seed == -1:
seed = np.random.randint(0, 10000)
print("Setting seed: {}".format(seed))
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
os.environ['PYTHONHASHSEED'] = str(seed)
torch.cuda.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
def parse_sim_params(args, cfg):
# code from Isaac Gym Preview 2
# initialize sim params
sim_params = gymapi.SimParams()
# set some values from args
if args.physics_engine == gymapi.SIM_FLEX:
if args.device != "cpu":
print("WARNING: Using Flex with GPU instead of PHYSX!")
elif args.physics_engine == gymapi.SIM_PHYSX:
sim_params.physx.use_gpu = args.use_gpu
sim_params.physx.num_subscenes = args.subscenes
sim_params.use_gpu_pipeline = args.use_gpu_pipeline
# if sim options are provided in cfg, parse them and update/override above:
if "sim" in cfg:
gymutil.parse_sim_config(cfg["sim"], sim_params)
# Override num_threads if passed on the command line
if args.physics_engine == gymapi.SIM_PHYSX and args.num_threads > 0:
sim_params.physx.num_threads = args.num_threads
return sim_params
def get_load_path(root, load_run=-1, checkpoint=-1):
try:
runs = os.listdir(root)
runs = [run for run in runs if len(list((Path(root) / run).glob('model_*.pt'))) > 0]
#TODO sort by date to handle change of month
runs.sort()
if 'exported' in runs: runs.remove('exported')
last_run = os.path.join(root, runs[-1])
except:
raise ValueError("No runs in this directory: " + root)
if load_run==-1:
load_run = last_run
else:
load_run = os.path.join(root, load_run)
if checkpoint==-1:
models = [file for file in os.listdir(load_run) if 'model' in file]
models.sort(key=lambda m: '{0:0>15}'.format(m))
model = models[-1]
else:
model = "model_{}.pt".format(checkpoint)
load_path = os.path.join(load_run, model)
return load_path
def update_cfg_from_args(env_cfg, cfg_train, args):
# seed
if env_cfg is not None:
# num envs
if args.num_envs is not None:
env_cfg.env.num_envs = args.num_envs
if cfg_train is not None:
if args.seed is not None:
cfg_train.seed = args.seed
# alg runner parameters
if args.max_iterations is not None:
cfg_train.runner.max_iterations = args.max_iterations
if args.resume:
cfg_train.runner.resume = args.resume
if args.experiment_name is not None:
cfg_train.runner.experiment_name = args.experiment_name
if args.run_name is not None:
cfg_train.runner.run_name = args.run_name
if args.load_run is not None:
cfg_train.runner.load_run = args.load_run
if args.checkpoint is not None:
cfg_train.runner.checkpoint = args.checkpoint
return env_cfg, cfg_train
def get_args():
custom_parameters = [
{"name": "--task", "type": str, "default": "go2", "help": "Resume training or start testing from a checkpoint. Overrides config file if provided."},
{"name": "--resume", "action": "store_true", "default": False, "help": "Resume training from a checkpoint"},
{"name": "--experiment_name", "type": str, "help": "Name of the experiment to run or load. Overrides config file if provided."},
{"name": "--run_name", "type": str, "help": "Name of the run. Overrides config file if provided."},
{"name": "--load_run", "type": str, "help": "Name of the run to load when resume=True. If -1: will load the last run. Overrides config file if provided."},
{"name": "--checkpoint", "type": int, "help": "Saved model checkpoint number. If -1: will load the last checkpoint. Overrides config file if provided."},
{"name": "--headless", "action": "store_true", "default": False, "help": "Force display off at all times"},
{"name": "--horovod", "action": "store_true", "default": False, "help": "Use horovod for multi-gpu training"},
{"name": "--rl_device", "type": str, "default": "cuda:0", "help": 'Device used by the RL algorithm, (cpu, gpu, cuda:0, cuda:1 etc..)'},
{"name": "--num_envs", "type": int, "help": "Number of environments to create. Overrides config file if provided."},
{"name": "--seed", "type": int, "help": "Random seed. Overrides config file if provided."},
{"name": "--max_iterations", "type": int, "help": "Maximum number of training iterations. Overrides config file if provided."},
]
# parse arguments
args = gymutil.parse_arguments(
description="RL Policy",
custom_parameters=custom_parameters)
# name allignment
args.sim_device_id = args.compute_device_id
args.sim_device = args.sim_device_type
if args.sim_device=='cuda':
args.sim_device += f":{args.sim_device_id}"
return args
# def export_policy_as_jit(actor_critic, path):
# if hasattr(actor_critic, 'memory_a'):
# # assumes LSTM: TODO add GRU
# exporter = PolicyExporterLSTM(actor_critic)
# exporter.export(path)
# else:
# os.makedirs(path, exist_ok=True)
# path = os.path.join(path, 'policy_1.pt')
# model = copy.deepcopy(actor_critic.actor).to('cpu')
# traced_script_module = torch.jit.script(model)
# traced_script_module.save(path)
# class PolicyExporterLSTM(torch.nn.Module):
# def __init__(self, actor_critic):
# super().__init__()
# self.actor = copy.deepcopy(actor_critic.actor)
# self.is_recurrent = actor_critic.is_recurrent
# self.memory = copy.deepcopy(actor_critic.memory_a.rnn)
# self.memory.cpu()
# self.register_buffer(f'hidden_state', torch.zeros(self.memory.num_layers, 1, self.memory.hidden_size))
# self.register_buffer(f'cell_state', torch.zeros(self.memory.num_layers, 1, self.memory.hidden_size))
# def forward(self, x):
# out, (h, c) = self.memory(x.unsqueeze(0), (self.hidden_state, self.cell_state))
# self.hidden_state[:] = h
# self.cell_state[:] = c
# return self.actor(out.squeeze(0))
# @torch.jit.export
# def reset_memory(self):
# self.hidden_state[:] = 0.
# self.cell_state[:] = 0.
# def export(self, path):
# os.makedirs(path, exist_ok=True)
# path = os.path.join(path, 'policy_lstm_1.pt')
# self.to('cpu')
# traced_script_module = torch.jit.script(self)
# traced_script_module.save(path)

View File

@@ -0,0 +1,47 @@
import os
import numpy as np
import random
import torch
@torch.jit.script
def copysign(a, b):
# type: (float, Tensor) -> Tensor
a = torch.tensor(a, device=b.device, dtype=torch.float).repeat(b.shape[0])
return torch.abs(a) * torch.sign(b)
def get_euler_xyz(q):
qx, qy, qz, qw = 0, 1, 2, 3
# roll (x-axis rotation)
sinr_cosp = 2.0 * (q[:, qw] * q[:, qx] + q[:, qy] * q[:, qz])
cosr_cosp = q[:, qw] * q[:, qw] - q[:, qx] * \
q[:, qx] - q[:, qy] * q[:, qy] + q[:, qz] * q[:, qz]
roll = torch.atan2(sinr_cosp, cosr_cosp)
# pitch (y-axis rotation)
sinp = 2.0 * (q[:, qw] * q[:, qy] - q[:, qz] * q[:, qx])
pitch = torch.where(
torch.abs(sinp) >= 1, copysign(np.pi / 2.0, sinp), torch.asin(sinp))
# yaw (z-axis rotation)
siny_cosp = 2.0 * (q[:, qw] * q[:, qz] + q[:, qx] * q[:, qy])
cosy_cosp = q[:, qw] * q[:, qw] + q[:, qx] * \
q[:, qx] - q[:, qy] * q[:, qy] - q[:, qz] * q[:, qz]
yaw = torch.atan2(siny_cosp, cosy_cosp)
return torch.stack((roll, pitch, yaw), dim=-1)
def sample_disjoint_intervals(env_ids, limit_bound, cfg_min, cfg_max, device):
"""
sample uniform distribution from [cfg_min, -limit_bound] U [limit_bound, cfg_max]
"""
width_neg = torch.nn.functional.relu(-limit_bound - cfg_min)
width_pos = torch.nn.functional.relu(cfg_max - limit_bound)
total_width = width_neg + width_pos + 1e-6 # 加极小值防除零
u = torch.rand(len(env_ids), device=device) * total_width
samples = torch.where(
u < width_neg,
cfg_min + u,
cfg_max - width_pos + (u - width_neg)
)
return samples

View File

@@ -0,0 +1,39 @@
import numpy as np
from collections import defaultdict
from multiprocessing import Process, Value
class Logger:
def __init__(self, dt):
self.state_log = defaultdict(list)
self.rew_log = defaultdict(list)
self.dt = dt
self.num_episodes = 0
self.plot_process = None
def log_state(self, key, value):
self.state_log[key].append(value)
def log_states(self, dict):
for key, value in dict.items():
self.log_state(key, value)
def log_rewards(self, dict, num_episodes):
for key, value in dict.items():
if 'rew' in key:
self.rew_log[key].append(value.item() * num_episodes)
self.num_episodes += num_episodes
def reset(self):
self.state_log.clear()
self.rew_log.clear()
def print_rewards(self):
print("Average rewards per second:")
for key, values in self.rew_log.items():
mean = np.sum(np.array(values)) / self.num_episodes
print(f" - {key}: {mean}")
print(f"Total number of episodes: {self.num_episodes}")
def __del__(self):
if self.plot_process is not None:
self.plot_process.kill()

26
legged_gym/utils/math.py Normal file
View File

@@ -0,0 +1,26 @@
import torch
from torch import Tensor
import numpy as np
from isaacgym.torch_utils import quat_apply, normalize
from typing import Tuple
# @ torch.jit.script
def quat_apply_yaw(quat, vec):
quat_yaw = quat.clone().view(-1, 4)
quat_yaw[:, :2] = 0.
quat_yaw = normalize(quat_yaw)
return quat_apply(quat_yaw, vec)
# @ torch.jit.script
def wrap_to_pi(angles):
angles %= 2*np.pi
angles -= 2*np.pi * (angles > np.pi)
return angles
# @ torch.jit.script
def torch_rand_sqrt_float(lower, upper, shape, device):
# type: (float, float, Tuple[int, int], str) -> Tensor
r = 2*torch.rand(*shape, device=device) - 1
r = torch.where(r<0., -torch.sqrt(-r), torch.sqrt(r))
r = (r + 1.) / 2.
return (upper - lower) * r + lower

View File

@@ -0,0 +1,129 @@
import os
from datetime import datetime
from typing import Tuple
import torch
import numpy as np
import sys
from rsl_rl.env import VecEnv
from rsl_rl.runners import OnPolicyRunner, OnPolicyRunnerCTS
from legged_gym import LEGGED_GYM_ROOT_DIR, LEGGED_GYM_ENVS_DIR
from .helpers import get_args, update_cfg_from_args, class_to_dict, get_load_path, set_seed, parse_sim_params
from legged_gym.envs.base.legged_robot_config import LeggedRobotCfg, LeggedRobotCfgPPO
class TaskRegistry():
def __init__(self):
self.task_classes = {}
self.env_cfgs = {}
self.train_cfgs = {}
def register(self, name: str, task_class: VecEnv, env_cfg: LeggedRobotCfg, train_cfg: LeggedRobotCfgPPO):
self.task_classes[name] = task_class
self.env_cfgs[name] = env_cfg
self.train_cfgs[name] = train_cfg
def get_task_class(self, name: str) -> VecEnv:
return self.task_classes[name]
def get_cfgs(self, name) -> Tuple[LeggedRobotCfg, LeggedRobotCfgPPO]:
train_cfg = self.train_cfgs[name]
env_cfg = self.env_cfgs[name]
# copy seed
env_cfg.seed = train_cfg.seed
return env_cfg, train_cfg
def make_env(self, name, args=None, env_cfg=None) -> Tuple[VecEnv, LeggedRobotCfg]:
""" Creates an environment either from a registered namme or from the provided config file.
Args:
name (string): Name of a registered env.
args (Args, optional): Isaac Gym comand line arguments. If None get_args() will be called. Defaults to None.
env_cfg (Dict, optional): Environment config file used to override the registered config. Defaults to None.
Raises:
ValueError: Error if no registered env corresponds to 'name'
Returns:
isaacgym.VecTaskPython: The created environment
Dict: the corresponding config file
"""
# if no args passed get command line arguments
if args is None:
args = get_args()
# check if there is a registered env with that name
if name in self.task_classes:
task_class = self.get_task_class(name)
else:
raise ValueError(f"Task with name: {name} was not registered")
if env_cfg is None:
# load config files
env_cfg, _ = self.get_cfgs(name)
# override cfg from args (if specified)
env_cfg, _ = update_cfg_from_args(env_cfg, None, args)
set_seed(env_cfg.seed)
# parse sim params (convert to dict first)
sim_params = {"sim": class_to_dict(env_cfg.sim)}
sim_params = parse_sim_params(args, sim_params)
env = task_class( cfg=env_cfg,
sim_params=sim_params,
physics_engine=args.physics_engine,
sim_device=args.sim_device,
headless=args.headless)
return env, env_cfg
def make_alg_runner(self, env, name=None, args=None, train_cfg=None, log_root="default") -> Tuple[OnPolicyRunner, LeggedRobotCfgPPO]:
""" Creates the training algorithm either from a registered namme or from the provided config file.
Args:
env (isaacgym.VecTaskPython): The environment to train (TODO: remove from within the algorithm)
name (string, optional): Name of a registered env. If None, the config file will be used instead. Defaults to None.
args (Args, optional): Isaac Gym comand line arguments. If None get_args() will be called. Defaults to None.
train_cfg (Dict, optional): Training config file. If None 'name' will be used to get the config file. Defaults to None.
log_root (str, optional): Logging directory for Tensorboard. Set to 'None' to avoid logging (at test time for example).
Logs will be saved in <log_root>/<date_time>_<run_name>. Defaults to "default"=<path_to_LEGGED_GYM>/logs/<experiment_name>.
Raises:
ValueError: Error if neither 'name' or 'train_cfg' are provided
Warning: If both 'name' or 'train_cfg' are provided 'name' is ignored
Returns:
PPO: The created algorithm
Dict: the corresponding config file
"""
# if no args passed get command line arguments
if args is None:
args = get_args()
# if config files are passed use them, otherwise load from the name
if train_cfg is None:
if name is None:
raise ValueError("Either 'name' or 'train_cfg' must be not None")
# load config files
_, train_cfg = self.get_cfgs(name)
else:
if name is not None:
print(f"'train_cfg' provided -> Ignoring 'name={name}'")
# override cfg from args (if specified)
_, train_cfg = update_cfg_from_args(None, train_cfg, args)
if log_root=="default":
log_root = os.path.join(LEGGED_GYM_ROOT_DIR, 'logs', train_cfg.runner.experiment_name)
log_dir = os.path.join(log_root, datetime.now().strftime('%b%d_%H-%M-%S') + '_' + train_cfg.runner.run_name)
elif log_root is None:
log_dir = None
else:
log_dir = os.path.join(log_root, datetime.now().strftime('%b%d_%H-%M-%S') + '_' + train_cfg.runner.run_name)
train_cfg_dict = class_to_dict(train_cfg)
runner = eval(train_cfg.runner_class_name)(env, train_cfg_dict, log_dir, device=args.rl_device)
#save resume path before creating a new log_dir
resume = train_cfg.runner.resume
if resume:
# load previously trained model
resume_path = get_load_path(log_root, load_run=train_cfg.runner.load_run, checkpoint=train_cfg.runner.checkpoint)
print(f"Loading model from: {resume_path}")
runner.load(resume_path)
return runner, train_cfg
# make global task registry
task_registry = TaskRegistry()

188
legged_gym/utils/terrain.py Normal file
View File

@@ -0,0 +1,188 @@
from collections import defaultdict
import numpy as np
from numpy.random import choice
from scipy import interpolate
from isaacgym import terrain_utils
from legged_gym.envs.base.legged_robot_config import LeggedRobotCfg
class Terrain:
def __init__(self, cfg: LeggedRobotCfg.terrain, num_robots) -> None:
self.cfg = cfg
self.num_robots = num_robots
self.type = cfg.mesh_type
if self.type in ["none", 'plane']:
return
self.env_length = cfg.terrain_length
self.env_width = cfg.terrain_width
self.proportions = [np.sum(cfg.terrain_proportions[:i+1]) for i in range(len(cfg.terrain_proportions))]
self.cfg.num_sub_terrains = cfg.num_rows * cfg.num_cols
self.env_origins = np.zeros((cfg.num_rows, cfg.num_cols, 3))
self.width_per_env_pixels = int(self.env_width / cfg.horizontal_scale)
self.length_per_env_pixels = int(self.env_length / cfg.horizontal_scale)
self.spacing = cfg.terrain_spacing
self.spacing_pixels = int(self.spacing / cfg.horizontal_scale)
self.border = int(cfg.border_size/self.cfg.horizontal_scale)
self.tot_cols = int(cfg.num_cols * self.width_per_env_pixels + max(0, cfg.num_cols-1) * self.spacing_pixels) + 2 * self.border
self.tot_rows = int(cfg.num_rows * self.length_per_env_pixels + max(0, cfg.num_rows-1) * self.spacing_pixels) + 2 * self.border
self.name2cols = defaultdict(set) # terrain type to column index
self.cols2id = [] # column index to terrain id
self.height_field_raw = np.zeros((self.tot_rows , self.tot_cols), dtype=np.int16)
if cfg.curriculum:
self.curiculum()
elif cfg.selected:
self.selected_terrain()
else:
self.randomized_terrain()
self.heightsamples = self.height_field_raw
if self.type=="trimesh":
self.vertices, self.triangles = terrain_utils.convert_heightfield_to_trimesh( self.height_field_raw,
self.cfg.horizontal_scale,
self.cfg.vertical_scale,
self.cfg.slope_treshold)
def randomized_terrain(self):
for k in range(self.cfg.num_sub_terrains):
# Env coordinates in the world
(i, j) = np.unravel_index(k, (self.cfg.num_rows, self.cfg.num_cols))
choice = np.random.uniform(0, 1)
difficulty = np.random.choice([0.5, 0.75, 0.9])
terrain = self.make_terrain(choice, difficulty)
self.add_terrain_to_map(terrain, i, j)
def curiculum(self):
for j in range(self.cfg.num_cols):
for i in range(self.cfg.num_rows):
difficulty = i / self.cfg.num_rows
choice = j / self.cfg.num_cols + 0.001
terrain = self.make_terrain(choice, difficulty)
self.add_terrain_to_map(terrain, i, j)
self.name2cols[terrain.terrain_name].add(j)
self.cols2id.append(terrain.terrain_id)
def selected_terrain(self):
terrain_type = self.cfg.terrain_kwargs.pop('type')
for k in range(self.cfg.num_sub_terrains):
# Env coordinates in the world
(i, j) = np.unravel_index(k, (self.cfg.num_rows, self.cfg.num_cols))
terrain = terrain_utils.SubTerrain("terrain",
width=self.width_per_env_pixels,
length=self.width_per_env_pixels,
vertical_scale=self.vertical_scale,
horizontal_scale=self.horizontal_scale)
eval(terrain_type)(terrain, **self.cfg.terrain_kwargs.terrain_kwargs)
self.add_terrain_to_map(terrain, i, j)
def make_terrain(self, choice, difficulty):
terrain = terrain_utils.SubTerrain("terrain",
width=self.width_per_env_pixels,
length=self.width_per_env_pixels,
vertical_scale=self.cfg.vertical_scale,
horizontal_scale=self.cfg.horizontal_scale)
slope = 0.1 + difficulty * 0.52
step_height = 0.05 + 0.23 * difficulty
discrete_obstacles_height = 0.05 + difficulty * 0.25
stepping_stones_size = 1.5 * (1.05 - difficulty)
stone_distance = 0.05 if difficulty==0 else 0.1
gap_size = 1. * difficulty
# pit_depth = 1. * difficulty # 删除坑洞
amplitude = 0.1 + 0.2 * difficulty
if choice < self.proportions[0]:
terrain.terrain_name = "wave"
terrain.terrain_id = 0
terrain_utils.wave_terrain(terrain, num_waves=5, amplitude=amplitude)
terrain_utils.random_uniform_terrain(terrain, min_height=-0.05, max_height=0.05, step=0.005, downsampled_scale=0.2)
elif choice < self.proportions[1]: # 平滑坡
terrain.terrain_name = "slope"
terrain.terrain_id = 1
if choice < (self.proportions[0] + self.proportions[1])/ 2: # 一半正坡, 一半负坡
slope *= -1
terrain_utils.pyramid_sloped_terrain(terrain, slope=slope, platform_size=3.)
elif choice < self.proportions[2]: # 粗糙坡
terrain.terrain_name = "rough_slope"
terrain.terrain_id = 2
terrain_utils.pyramid_sloped_terrain(terrain, slope=slope, platform_size=3.)
terrain_utils.random_uniform_terrain(terrain, min_height=-0.05, max_height=0.05, step=0.005, downsampled_scale=0.2)
elif choice < self.proportions[4]: # 下楼梯
terrain.terrain_name = "stairs_down"
terrain.terrain_id = 4
if choice<self.proportions[3]: # 上楼梯
terrain.terrain_name = "stairs_up"
terrain.terrain_id = 3
step_height *= -1
terrain_utils.pyramid_stairs_terrain(terrain, step_width=0.31, step_height=step_height, platform_size=3.)
elif choice < self.proportions[5]: # 障碍物
terrain.terrain_name = "obstacles"
terrain.terrain_id = 5
num_rectangles = 20
rectangle_min_size = 1.
rectangle_max_size = 2.
terrain_utils.discrete_obstacles_terrain(terrain, discrete_obstacles_height, rectangle_min_size, rectangle_max_size, num_rectangles, platform_size=3.)
elif choice < self.proportions[6]: # 梅花桩
terrain.terrain_name = "stepping_stones"
terrain.terrain_id = 6
terrain_utils.stepping_stones_terrain(terrain, stone_size=stepping_stones_size, stone_distance=stone_distance, max_height=0., platform_size=4.)
elif choice < self.proportions[7]: # 间隙
terrain.terrain_name = "gap"
terrain.terrain_id = 7
gap_terrain(terrain, gap_size=gap_size, platform_size=3.)
else: # 平地
terrain.terrain_name = "flat"
terrain.terrain_id = 8
pit_terrain(terrain, depth=0.0, platform_size=4.)
return terrain
def add_terrain_to_map(self, terrain, row, col):
i = row
j = col
# map coordinate system
start_x = self.border + i * (self.length_per_env_pixels + self.spacing_pixels)
end_x = start_x + self.length_per_env_pixels
start_y = self.border + j * (self.width_per_env_pixels + self.spacing_pixels)
end_y = start_y + self.width_per_env_pixels
self.height_field_raw[start_x: end_x, start_y:end_y] = terrain.height_field_raw
env_origin_x = (i + 0.5) * self.env_length + i * self.spacing
env_origin_y = (j + 0.5) * self.env_width + j * self.spacing
x1 = int((self.env_length/2. - 1) / terrain.horizontal_scale)
x2 = int((self.env_length/2. + 1) / terrain.horizontal_scale)
y1 = int((self.env_width/2. - 1) / terrain.horizontal_scale)
y2 = int((self.env_width/2. + 1) / terrain.horizontal_scale)
env_origin_z = np.max(terrain.height_field_raw[x1:x2, y1:y2])*terrain.vertical_scale
self.env_origins[i, j] = [env_origin_x, env_origin_y, env_origin_z]
def gap_terrain(terrain, gap_size, platform_size=1.):
gap_size = int(gap_size / terrain.horizontal_scale)
platform_size = int(platform_size / terrain.horizontal_scale)
center_x = terrain.length // 2
center_y = terrain.width // 2
x1 = (terrain.length - platform_size) // 2
x2 = x1 + gap_size
y1 = (terrain.width - platform_size) // 2
y2 = y1 + gap_size
terrain.height_field_raw[center_x-x2 : center_x + x2, center_y-y2 : center_y + y2] = -1000
terrain.height_field_raw[center_x-x1 : center_x + x1, center_y-y1 : center_y + y1] = 0
def pit_terrain(terrain, depth, platform_size=1.):
depth = int(depth / terrain.vertical_scale)
platform_size = int(platform_size / terrain.horizontal_scale / 2)
x1 = terrain.length // 2 - platform_size
x2 = terrain.length // 2 + platform_size
y1 = terrain.width // 2 - platform_size
y2 = terrain.width // 2 + platform_size
terrain.height_field_raw[x1:x2, y1:y2] = -depth

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 919 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,22 @@
<mujoco model="go2 scene">
<include file="go2.xml"/>
<statistic center="1.0 0.7 1.0" extent="0.8"/>
<visual>
<headlight diffuse="0.6 0.6 0.6" ambient="0.1 0.1 0.1" specular="0.9 0.9 0.9"/>
<rgba haze="0.15 0.25 0.35 1"/>
<global azimuth="-140" elevation="-20"/>
</visual>
<asset>
<texture type="skybox" builtin="flat" rgb1="0 0 0" rgb2="0 0 0" width="512" height="3072"/>
<texture type="2d" name="groundplane" builtin="checker" mark="edge" rgb1="0.2 0.3 0.4" rgb2="0.1 0.2 0.3" markrgb="0.8 0.8 0.8" width="300" height="300"/>
<material name="groundplane" texture="groundplane" texuniform="true" texrepeat="5 5" reflectance="0.2"/>
</asset>
<worldbody>
<light pos="1 0 3.5" dir="0 0 -1" directional="true"/>
<geom name="floor" size="0 0 0.05" type="plane" material="groundplane"/>
</worldbody>
</mujoco>

View File

@@ -0,0 +1,184 @@
<mujoco model="go2_description">
<compiler angle="radian" meshdir="assets"/>
<statistic meansize="0.144785" extent="1.23314" center="0.025392 2.0634e-05 -0.245975"/>
<default>
<joint damping="0.001" armature="0.01" frictionloss="0.1"/>
<default class="visual">
<geom type="mesh" contype="0" conaffinity="0" group="2"/>
</default>
</default>
<asset>
<material name="metal" rgba=".9 .95 .95 1" />
<material name="black" rgba="0 0 0 1" />
<material name="white" rgba="1 1 1 1" />
<material name="gray" rgba="0.671705 0.692426 0.774270 1" />
<mesh file="base_0.obj" />
<mesh file="base_1.obj" />
<mesh file="base_2.obj" />
<mesh file="base_3.obj" />
<mesh file="base_4.obj" />
<mesh file="hip_0.obj" />
<mesh file="hip_1.obj" />
<mesh file="thigh_0.obj" />
<mesh file="thigh_1.obj" />
<mesh file="thigh_mirror_0.obj" />
<mesh file="thigh_mirror_1.obj" />
<mesh file="calf_0.obj" />
<mesh file="calf_1.obj" />
<mesh file="calf_mirror_0.obj" />
<mesh file="calf_mirror_1.obj" />
<mesh file="foot.obj" />
</asset>
<worldbody>
<body name="base" pos="0 0 0.445">
<!-- <body name="base" pos="-5 2 0.445"> -->
<!-- <body name="base" pos="-5 2 0.30" quat="0 0 0.706 0.706"> -->
<!-- <body name="base" pos="-5 2 0.445" quat="0 0 1 0"> -->
<!-- <body name="base" pos="0 0 0.16" quat="0 0 0.706 0.706"> -->
<!-- <body name="base" pos="0 0 0.1" quat="0 0 1 0"> -->
<inertial pos="0.021112 0 -0.005366"
mass="6.921"
diaginertia="0.02448 0.098077 0.107" />
<freejoint name="root"/>
<geom mesh="base_0" material="black" class="visual" />
<geom mesh="base_1" material="black" class="visual" />
<geom mesh="base_2" material="black" class="visual" />
<geom mesh="base_3" material="white" class="visual" />
<geom mesh="base_4" material="gray" class="visual" />
<geom name="base_box" type="box" size="0.1881 0.04675 0.057"/>
<geom name="base_cyl1" type="cylinder" size="0.05 0.045" pos="0.285 0 0.01"/>
<geom name="base_sphere1" type="sphere" size="0.047" pos="0.293 0 -0.06"/>
<body name="FL_hip" pos="0.1934 0.0465 0">
<inertial pos="-0.0054 0.00194 -0.000105" quat="0.497014 0.499245 0.505462 0.498237" mass="0.678" diaginertia="0.00088403 0.000596003 0.000479967"/>
<joint name="FL_hip_joint" pos="0 0 0" axis="1 0 0" range="-1.0472 1.0472" actuatorfrcrange="-23.7 23.7"/>
<geom mesh="hip_0" material="metal" class="visual" />
<geom mesh="hip_1" material="gray" class="visual" />
<geom size="0.046 0.02" pos="0 0.08 0" quat="0.707107 0.707107 0 0" type="cylinder" rgba="1 1 1 1"/>
<body name="FL_thigh" pos="0 0.0955 0">
<inertial pos="-0.00374 -0.0223 -0.0327" quat="0.829533 0.0847635 -0.0200632 0.551623" mass="1.152" diaginertia="0.00594973 0.00584149 0.000878787"/>
<joint name="FL_thigh_joint" pos="0 0 0" axis="0 1 0" range="-1.5708 3.4907" actuatorfrcrange="-23.7 23.7"/>
<geom mesh="thigh_0" material="metal" class="visual" />
<geom mesh="thigh_1" material="gray" class="visual" />
<geom size="0.055 0.01225 0.017" pos="0 0 -0.1065" quat="0.707107 0 0.707107 0" type="box" rgba="1 1 1 1"/>
<body name="FL_calf" pos="0 0 -0.213">
<inertial pos="0.00629595 -0.000622121 -0.141417" quat="0.710672 0.00154099 -0.00450087 0.703508" mass="0.241352" diaginertia="0.0014901 0.00146356 5.31397e-05"/>
<joint name="FL_calf_joint" pos="0 0 0" axis="0 1 0" range="-2.7227 -0.83776" actuatorfrcrange="-35.55 35.55"/>
<geom mesh="calf_0" material="gray" class="visual" />
<geom mesh="calf_1" material="black" class="visual" />
<geom size="0.012 0.06" pos="0.008 0 -0.06" quat="0.994493 0 -0.104807 0" type="cylinder" rgba="1 1 1 1"/>
<geom size="0.011 0.0325" pos="0.02 0 -0.148" quat="0.999688 0 0.0249974 0" type="cylinder"/>
<geom size="0.0155 0.015" pos="0.00801333 0 -0.18745" quat="0.965093 0 0.261909 0" type="cylinder"/>
<geom pos="0 0 -0.213" mesh="foot" class="visual" material="black" />
<geom size="0.022" pos="-0.002 0 -0.213" rgba="1 1 1 1"/>
</body>
</body>
</body>
<body name="FR_hip" pos="0.1934 -0.0465 0">
<inertial pos="-0.0054 -0.00194 -0.000105" quat="0.498237 0.505462 0.499245 0.497014" mass="0.678" diaginertia="0.00088403 0.000596003 0.000479967"/>
<joint name="FR_hip_joint" pos="0 0 0" axis="1 0 0" range="-1.0472 1.0472" actuatorfrcrange="-23.7 23.7"/>
<geom mesh="hip_0" material="metal" class="visual" quat="4.63268e-05 1 0 0" />
<geom mesh="hip_1" material="gray" class="visual" quat="4.63268e-05 1 0 0" />
<geom size="0.046 0.02" pos="0 -0.08 0" quat="0.707107 0.707107 0 0" type="cylinder" rgba="1 1 1 1"/>
<body name="FR_thigh" pos="0 -0.0955 0">
<inertial pos="-0.00374 0.0223 -0.0327" quat="0.551623 -0.0200632 0.0847635 0.829533" mass="1.152" diaginertia="0.00594973 0.00584149 0.000878787"/>
<joint name="FR_thigh_joint" pos="0 0 0" axis="0 1 0" range="-1.5708 3.4907" actuatorfrcrange="-23.7 23.7"/>
<geom mesh="thigh_mirror_0" material="metal" class="visual" />
<geom mesh="thigh_mirror_1" material="gray" class="visual" />
<geom size="0.055 0.01225 0.017" pos="0 0 -0.1065" quat="0.707107 0 0.707107 0" type="box" rgba="1 1 1 1"/>
<body name="FR_calf" pos="0 0 -0.213">
<inertial pos="0.00629595 0.000622121 -0.141417" quat="0.703508 -0.00450087 0.00154099 0.710672" mass="0.241352" diaginertia="0.0014901 0.00146356 5.31397e-05"/>
<joint name="FR_calf_joint" pos="0 0 0" axis="0 1 0" range="-2.7227 -0.83776" actuatorfrcrange="-35.55 35.55"/>
<geom mesh="calf_mirror_0" material="gray" class="visual" />
<geom mesh="calf_mirror_1" material="black" class="visual" />
<geom size="0.013 0.06" pos="0.01 0 -0.06" quat="0.995004 0 -0.0998334 0" type="cylinder" rgba="1 1 1 1"/>
<geom size="0.011 0.0325" pos="0.02 0 -0.148" quat="0.999688 0 0.0249974 0" type="cylinder"/>
<geom size="0.0155 0.015" pos="0.00801333 0 -0.18745" quat="0.965093 0 0.261909 0" type="cylinder"/>
<geom pos="0 0 -0.213" mesh="foot" class="visual" material="black" />
<geom size="0.022" pos="-0.002 0 -0.213" rgba="1 1 1 1"/>
</body>
</body>
</body>
<body name="RL_hip" pos="-0.1934 0.0465 0">
<inertial pos="0.0054 0.00194 -0.000105" quat="0.505462 0.498237 0.497014 0.499245" mass="0.678" diaginertia="0.00088403 0.000596003 0.000479967"/>
<joint name="RL_hip_joint" pos="0 0 0" axis="1 0 0" range="-1.0472 1.0472" actuatorfrcrange="-23.7 23.7"/>
<geom mesh="hip_0" material="metal" class="visual" quat="4.63268e-05 0 1 0" />
<geom mesh="hip_1" material="gray" class="visual" quat="4.63268e-05 0 1 0" />
<geom size="0.046 0.02" pos="0 0.08 0" quat="0.707107 0.707107 0 0" type="cylinder" rgba="1 1 1 1"/>
<body name="RL_thigh" pos="0 0.0955 0">
<inertial pos="-0.00374 -0.0223 -0.0327" quat="0.829533 0.0847635 -0.0200632 0.551623" mass="1.152" diaginertia="0.00594973 0.00584149 0.000878787"/>
<joint name="RL_thigh_joint" pos="0 0 0" axis="0 1 0" range="-0.5236 4.5379" actuatorfrcrange="-23.7 23.7"/>
<geom mesh="thigh_0" material="metal" class="visual" />
<geom mesh="thigh_1" material="gray" class="visual" />
<geom size="0.055 0.01225 0.017" pos="0 0 -0.1065" quat="0.707107 0 0.707107 0" type="box" rgba="1 1 1 1"/>
<body name="RL_calf" pos="0 0 -0.213">
<inertial pos="0.00629595 -0.000622121 -0.141417" quat="0.710672 0.00154099 -0.00450087 0.703508" mass="0.241352" diaginertia="0.0014901 0.00146356 5.31397e-05"/>
<joint name="RL_calf_joint" pos="0 0 0" axis="0 1 0" range="-2.7227 -0.83776" actuatorfrcrange="-35.55 35.55"/>
<geom mesh="calf_0" material="gray" class="visual" />
<geom mesh="calf_1" material="black" class="visual" />
<geom size="0.013 0.06" pos="0.01 0 -0.06" quat="0.995004 0 -0.0998334 0" type="cylinder" rgba="1 1 1 1"/>
<geom size="0.011 0.0325" pos="0.02 0 -0.148" quat="0.999688 0 0.0249974 0" type="cylinder"/>
<geom size="0.0155 0.015" pos="0.00801333 0 -0.18745" quat="0.965093 0 0.261909 0" type="cylinder"/>
<geom pos="0 0 -0.213" mesh="foot" class="visual" material="black" />
<geom size="0.022" pos="-0.002 0 -0.213" rgba="1 1 1 1"/>
</body>
</body>
</body>
<body name="RR_hip" pos="-0.1934 -0.0465 0">
<inertial pos="0.0054 -0.00194 -0.000105" quat="0.499245 0.497014 0.498237 0.505462" mass="0.678" diaginertia="0.00088403 0.000596003 0.000479967"/>
<joint name="RR_hip_joint" pos="0 0 0" axis="1 0 0" range="-1.0472 1.0472" actuatorfrcrange="-23.7 23.7"/>
<geom mesh="hip_0" material="metal" class="visual"
quat="2.14617e-09 4.63268e-05 4.63268e-05 -1" />
<geom mesh="hip_1" material="gray" class="visual"
quat="2.14617e-09 4.63268e-05 4.63268e-05 -1" />
<geom size="0.046 0.02" pos="0 -0.08 0" quat="0.707107 0.707107 0 0" type="cylinder" rgba="1 1 1 1"/>
<body name="RR_thigh" pos="0 -0.0955 0">
<inertial pos="-0.00374 0.0223 -0.0327" quat="0.551623 -0.0200632 0.0847635 0.829533" mass="1.152" diaginertia="0.00594973 0.00584149 0.000878787"/>
<joint name="RR_thigh_joint" pos="0 0 0" axis="0 1 0" range="-0.5236 4.5379" actuatorfrcrange="-23.7 23.7"/>
<geom mesh="thigh_mirror_0" material="metal" class="visual" />
<geom mesh="thigh_mirror_1" material="gray" class="visual" />
<geom size="0.055 0.01225 0.017" pos="0 0 -0.1065" quat="0.707107 0 0.707107 0" type="box" rgba="1 1 1 1"/>
<body name="RR_calf" pos="0 0 -0.213">
<inertial pos="0.00629595 0.000622121 -0.141417" quat="0.703508 -0.00450087 0.00154099 0.710672" mass="0.241352" diaginertia="0.0014901 0.00146356 5.31397e-05"/>
<joint name="RR_calf_joint" pos="0 0 0" axis="0 1 0" range="-2.7227 -0.83776" actuatorfrcrange="-35.55 35.55"/>
<geom mesh="calf_mirror_0" material="gray" class="visual" />
<geom mesh="calf_mirror_1" material="black" class="visual" />
<geom size="0.013 0.06" pos="0.01 0 -0.06" quat="0.995004 0 -0.0998334 0" type="cylinder" rgba="1 1 1 1"/>
<geom size="0.011 0.0325" pos="0.02 0 -0.148" quat="0.999688 0 0.0249974 0" type="cylinder"/>
<geom size="0.0155 0.015" pos="0.00801333 0 -0.18745" quat="0.965093 0 0.261909 0" type="cylinder"/>
<geom pos="0 0 -0.213" mesh="foot" class="visual" material="black" />
<geom size="0.022" pos="-0.002 0 -0.213" rgba="1 1 1 1"/>
</body>
</body>
</body>
</body>
</worldbody>
<actuator>
<motor name="FL_hip" joint="FL_hip_joint"/>
<motor name="FL_thigh" joint="FL_thigh_joint"/>
<motor name="FL_calf" joint="FL_calf_joint"/>
<motor name="FR_hip" joint="FR_hip_joint"/>
<motor name="FR_thigh" joint="FR_thigh_joint"/>
<motor name="FR_calf" joint="FR_calf_joint"/>
<motor name="RL_hip" joint="RL_hip_joint"/>
<motor name="RL_thigh" joint="RL_thigh_joint"/>
<motor name="RL_calf" joint="RL_calf_joint"/>
<motor name="RR_hip" joint="RR_hip_joint"/>
<motor name="RR_thigh" joint="RR_thigh_joint"/>
<motor name="RR_calf" joint="RR_calf_joint"/>
</actuator>
</mujoco>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,807 @@
import xml.etree.ElementTree as xml_et
from pathlib import Path
import numpy as np
import cv2
import noise
import os
ROBOT = "go2"
INPUT_SCENE_PATH = os.path.join(os.path.dirname(__file__), "scene.xml")
OUTPUT_SCENE_PATH = os.path.join(os.path.dirname(__file__), "scene_terrain.xml")
PATH_DIR = Path(__file__).parent.absolute()
# zyx euler angle to quaternion
def euler_to_quat(roll, pitch, yaw):
cx = np.cos(roll / 2)
sx = np.sin(roll / 2)
cy = np.cos(pitch / 2)
sy = np.sin(pitch / 2)
cz = np.cos(yaw / 2)
sz = np.sin(yaw / 2)
return np.array(
[
cx * cy * cz + sx * sy * sz,
sx * cy * cz - cx * sy * sz,
cx * sy * cz + sx * cy * sz,
cx * cy * sz - sx * sy * cz,
],
dtype=np.float64,
)
# zyx euler angle to rotation matrix
def euler_to_rot(roll, pitch, yaw):
rot_x = np.array(
[
[1, 0, 0],
[0, np.cos(roll), -np.sin(roll)],
[0, np.sin(roll), np.cos(roll)],
],
dtype=np.float64,
)
rot_y = np.array(
[
[np.cos(pitch), 0, np.sin(pitch)],
[0, 1, 0],
[-np.sin(pitch), 0, np.cos(pitch)],
],
dtype=np.float64,
)
rot_z = np.array(
[
[np.cos(yaw), -np.sin(yaw), 0],
[np.sin(yaw), np.cos(yaw), 0],
[0, 0, 1],
],
dtype=np.float64,
)
return rot_z @ rot_y @ rot_x
# 2d rotate
def rot2d(x, y, yaw):
nx = x * np.cos(yaw) - y * np.sin(yaw)
ny = x * np.sin(yaw) + y * np.cos(yaw)
return nx, ny
# 3d rotate
def rot3d(pos, euler):
R = euler_to_rot(euler[0], euler[1], euler[2])
return R @ pos
def list_to_str(vec):
return " ".join(str(s) for s in vec)
class TerrainGenerator:
def __init__(self) -> None:
self.scene = xml_et.parse(INPUT_SCENE_PATH)
self.root = self.scene.getroot()
self.worldbody = self.root.find("worldbody")
self.asset = self.root.find("asset")
self._add_wood_material()
self._add_sponge_material()
def _add_wood_material(self):
# 1. 添加纹理 (Texture)
# 这里使用内置的 "flat" 类型配上棕色,模拟一种简单的木头颜色。
# 如果你有真实的木纹图片(比如 wood.png),请将 type="2d" builtin="flat"
# 改为 type="2d" file="../wood.png"
tex = xml_et.SubElement(self.asset, "texture")
tex.attrib["name"] = "wood_tex"
tex.attrib["type"] = "2d"
tex.attrib["file"] = "./assets/wood.png"
tex.attrib["rgb1"] = "0.6 0.4 0.2" # 棕色 (RGB)
tex.attrib["width"] = "512"
tex.attrib["height"] = "512"
# 2. 添加材质 (Material)
mat = xml_et.SubElement(self.asset, "material")
mat.attrib["name"] = "wood_mat" # 材质名称,后面 AddBox 要用
mat.attrib["texture"] = "wood_tex" # 关联上面的纹理
mat.attrib["specular"] = "0.2" # 木头反光度较低
mat.attrib["shininess"] = "0.1" # 亮度较低
mat.attrib["rgba"] = "1 1 1 1"
def _add_sponge_material(self):
# 1. 添加纹理 (Texture)
# 因为没有图片文件,我们使用 builtin="flat" 来生成纯色纹理
tex = xml_et.SubElement(self.asset, "texture")
tex.attrib["name"] = "sponge_tex"
tex.attrib["type"] = "2d"
tex.attrib["builtin"] = "flat" # 使用内置平面纹理,不需要 file 路径
tex.attrib["rgb1"] = "1.0 0.7 0.7" # 设置颜色:粉色 (参考图片颜色)
tex.attrib["width"] = "512"
tex.attrib["height"] = "512"
# 2. 添加材质 (Material)
mat = xml_et.SubElement(self.asset, "material")
mat.attrib["name"] = "mat_sponge" # 材质名称AddBox 中调用这个名字
mat.attrib["texture"] = "sponge_tex"
# 海绵的关键视觉特性:不反光、不油亮
mat.attrib["specular"] = "0.1" # 几乎没有镜面反射 (相比木头的0.2要低很多)
mat.attrib["shininess"] = "0.1" # 几乎没有光泽
mat.attrib["rgba"] = "1 0.7 0.7 1" # 叠加颜色,保持原样
# Add Box to scene
def AddBox(self,
position=[1.0, 0.0, 0.0],
euler=[0.0, 0.0, 0.0],
size=[0.1, 0.1, 0.1],
sponge=False):
geo = xml_et.SubElement(self.worldbody, "geom")
geo.attrib["pos"] = list_to_str(position)
geo.attrib["type"] = "box"
geo.attrib["size"] = list_to_str(
0.5 * np.array(size)) # half size of box for mujoco
quat = euler_to_quat(euler[0], euler[1], euler[2])
geo.attrib["quat"] = list_to_str(quat)
# === 修改部分开始 ===
if sponge:
# 1. 视觉:使用海绵材质 (假设你在 asset 中定义的名字叫 mat_sponge)
geo.attrib["material"] = "mat_sponge"
# 2. 物理solref 时间常数越大越软 (0.02 比较软, 默认约 0.002)
geo.attrib["solref"] = "0.03 1"
geo.attrib["priority"] = "1"
geo.attrib["solmix"] = "1"
# 3. 摩擦:海绵通常摩擦力较大 (可选)
geo.attrib["friction"] = "1.2 0.005 0.0001"
else:
geo.attrib["material"] = "wood_mat"
geo.attrib["friction"] = "0.5 0.005 0.0001"
def AddGeometry(self,
position=[1.0, 0.0, 0.0],
euler=[0.0, 0.0, 0.0],
size=[0.1, 0.1],geo_type="box"):
# geo_type supports "plane", "sphere", "capsule", "ellipsoid", "cylinder", "box"
geo = xml_et.SubElement(self.worldbody, "geom")
geo.attrib["pos"] = list_to_str(position)
geo.attrib["type"] = geo_type
geo.attrib["size"] = list_to_str(
0.5 * np.array(size)) # half size of box for mujoco
quat = euler_to_quat(euler[0], euler[1], euler[2])
geo.attrib["quat"] = list_to_str(quat)
geo.attrib["material"] = "wood_mat"
def AddStairs(self,
init_pos=[1.0, 0.0, 0.0],
yaw=0.0,
width=0.2,
height=0.15,
length=1.5,
stair_nums=10):
local_pos = [0.0, 0.0, -0.5 * height]
for i in range(stair_nums):
local_pos[0] += width
local_pos[2] += height
x, y = rot2d(local_pos[0], local_pos[1], yaw)
self.AddBox([x + init_pos[0], y + init_pos[1], local_pos[2]],
[0.0, 0.0, yaw], [width, length, height])
def AddDownStairs(self,
init_pos=[1.0, 0.0, 0.0],
yaw=0.0,
width=0.3,
height=0.15,
length=1.5,
stair_nums=10):
# 从上向下生成台阶:第一个台阶中心在 +0.5*height随后每步降低 height
local_pos = [0.0, 0.0, 0.5 * height]
for i in range(stair_nums):
local_pos[0] += width
local_pos[2] -= height
x, y = rot2d(local_pos[0], local_pos[1], yaw)
self.AddBox([x + init_pos[0], y + init_pos[1], local_pos[2]],
[0.0, 0.0, yaw], [width, length, height])
def AddStairsSeries(self,
init_pos=[1.0, 0.0, 0.0],
yaw=0.0,
width=0.3,
length=1.5,
stair_nums_up=6,
stair_nums_down=6,
start_height=0.05,
step_inc=0.03,
max_height=0.2,
flat_length=0.5):
"""生成一系列台阶先上台阶stair_nums_up顶部有一段平地flat_length再下台阶stair_nums_down
每完成一对上/下台阶后,单步高度增加 step_inc直到达到 max_height。
参数说明:
- init_pos: 底层起点(列表),序列沿局部 x 方向展开
- yaw: 台阶朝向
- width: 每级台阶在 x 方向的深度(步幅)
- length: 台阶在 y 方向的宽度(和 AddStairs 一致)
- stair_nums_up/down: 上/下台阶的级数
- start_height: 第一对台阶的每级高度
- step_inc: 每对增加的高度
- max_height: 最大每级高度(包含)
- flat_length: 顶部平地长度(沿 x
"""
# 保持 init_pos 不变(上/下台阶成对结束后回到同一基准高度)
base_pos = np.array(init_pos, dtype=float)
height = start_height
# 平台长度至少为 1.0 米
platform_length = max(flat_length, 1.0)
# 迭代每一对台阶直到高度超限
while height <= max_height + 1e-8:
# --- 上台阶 ---
# 上台阶第 i 级中心 z = base_z + (-0.5 + i) * height, i = 1..stair_nums_up
for i in range(1, stair_nums_up + 1):
center_x_local = i * width
center_z = base_pos[2] + (-0.5 + i) * height
x, y = rot2d(center_x_local, 0.0, yaw)
self.AddBox([x + base_pos[0], y + base_pos[1], center_z],
[0.0, 0.0, yaw], [width, length, height])
# 顶部平地:放在最后一级之后,平台长度至少 platform_length
last_up_center_x = stair_nums_up * width
last_up_top_surface = base_pos[2] + stair_nums_up * height # 顶部平面高度
flat_thickness = height # 平地厚度,使用与台阶同高度以保证接触
flat_center_local_x = last_up_center_x + width / 2.0 + platform_length / 2.0
flat_center_z = last_up_top_surface + flat_thickness / 2.0
x, y = rot2d(flat_center_local_x, 0.0, yaw)
# 尺寸:在 x 方向用 platform_length, y 用 length, z 用 flat_thickness
self.AddBox([x + base_pos[0], y + base_pos[1], flat_center_z],
[0.0, 0.0, yaw], [platform_length, length, flat_thickness])
# --- 下台阶 ---
# 平地末端 x
flat_end_x = last_up_center_x + width / 2.0 + platform_length
for j in range(1, stair_nums_down + 1):
center_x_local = flat_end_x + width / 2.0 + (j - 1) * width
# 第 j 级下台阶的中心 z = last_up_top_surface - 0.5*height - (j-1)*height
center_z = last_up_top_surface - 0.5 * height - (j - 1) * height
x, y = rot2d(center_x_local, 0.0, yaw)
self.AddBox([x + base_pos[0], y + base_pos[1], center_z],
[0.0, 0.0, yaw], [width, length, height])
# 下台阶之后也添加一段平地(连接到下一组上台阶),长度至少 platform_length
seq_end_local_x = flat_end_x + stair_nums_down * width # 这是最后一个下台阶的前缘 x
post_flat_center_local_x = seq_end_local_x + platform_length / 2.0
# 该平地应与下一组上台阶的起始高度对齐:其顶面与 base_z + height 对齐
post_flat_center_z = base_pos[2] + height / 2.0
x, y = rot2d(post_flat_center_local_x, 0.0, yaw)
self.AddBox([x + base_pos[0], y + base_pos[1], post_flat_center_z],
[0.0, 0.0, yaw], [platform_length, length, flat_thickness])
# 为下一对台阶准备:把 base_pos 在 x 方向平移到当前序列末端post flat 末端),保持 z 不变
seq_total_end_local_x = seq_end_local_x + platform_length
# 计算下一组基准位移,使下一组上台阶第一级的前缘与当前 post-flat 的末端无缝对接
# base_shift_local_x 为相对于当前 base 的局部 x 偏移
overlap = 1e-3 # 以米为单位,微小重叠以避免可视缝隙
base_shift_local_x = seq_total_end_local_x - width / 2.0 - overlap
dx, dy = rot2d(base_shift_local_x, 0.0, yaw)
base_pos[0] = base_pos[0] + dx
base_pos[1] = base_pos[1] + dy
# 增加单级高度
height = round(height + step_inc, 8)
def AddSuspendStairs(self,
init_pos=[1.0, 0.0, 0.0],
yaw=1.0,
width=0.2,
height=0.15,
length=1.5,
gap=0.1,
stair_nums=10):
local_pos = [0.0, 0.0, -0.5 * height]
for i in range(stair_nums):
local_pos[0] += width
local_pos[2] += height
x, y = rot2d(local_pos[0], local_pos[1], yaw)
self.AddBox([x + init_pos[0], y + init_pos[1], local_pos[2]],
[0.0, 0.0, yaw],
[width, length, abs(height - gap)])
def AddRoughGround(self,
init_pos=[1.0, 0.0, 0.0],
euler=[0.0, -0.0, 0.0],
nums=[10, 10],
box_size=[0.5, 0.5, 0.5],
box_euler=[0.0, 0.0, 0.0],
separation=[0.2, 0.2],
box_size_rand=[0.05, 0.05, 0.05],
box_euler_rand=[0.2, 0.2, 0.2],
separation_rand=[0.05, 0.05]):
local_pos = [0.0, 0.0, -0.5 * box_size[2]]
new_separation = np.array(separation) + np.array(
separation_rand) * np.random.uniform(-1.0, 1.0, 2)
for i in range(nums[0]):
local_pos[0] += new_separation[0]
local_pos[1] = 0.0
for j in range(nums[1]):
new_box_size = np.array(box_size) + np.array(
box_size_rand) * np.random.uniform(-1.0, 1.0, 3)
new_box_euler = np.array(box_euler) + np.array(
box_euler_rand) * np.random.uniform(-1.0, 1.0, 3)
new_separation = np.array(separation) + np.array(
separation_rand) * np.random.uniform(-1.0, 1.0, 2)
local_pos[1] += new_separation[1]
pos = rot3d(local_pos, euler) + np.array(init_pos)
self.AddBox(pos, new_box_euler, new_box_size)
def AddPerlinHeighField(
self,
position=[1.0, 0.0, 0.0], # position
euler=[0.0, -0.0, 0.0], # attitude
size=[1.0, 1.0], # width and length
height_scale=0.2, # max height
negative_height=0.2, # height in the negative direction of z axis
image_width=128, # height field image size
img_height=128,
smooth=100.0, # smooth scale
perlin_octaves=6, # perlin noise parameter
perlin_persistence=0.5,
perlin_lacunarity=2.0,
output_hfield_image="height_field.png"):
# Generating height field based on perlin noise
terrain_image = np.zeros((img_height, image_width), dtype=np.uint8)
for y in range(image_width):
for x in range(image_width):
# Perlin noise
noise_value = noise.pnoise2(x / smooth,
y / smooth,
octaves=perlin_octaves,
persistence=perlin_persistence,
lacunarity=perlin_lacunarity)
terrain_image[y, x] = int((noise_value + 1) / 2 * 255)
cv2.imwrite(str(PATH_DIR / "assets" / output_hfield_image), terrain_image)
hfield = xml_et.SubElement(self.asset, "hfield")
hfield.attrib["name"] = "perlin_hfield"
hfield.attrib["size"] = list_to_str(
[size[0] / 2.0, size[1] / 2.0, height_scale, negative_height])
hfield.attrib["file"] = "../" + output_hfield_image
geo = xml_et.SubElement(self.worldbody, "geom")
geo.attrib["type"] = "hfield"
geo.attrib["hfield"] = "perlin_hfield"
geo.attrib["pos"] = list_to_str(position)
quat = euler_to_quat(euler[0], euler[1], euler[2])
geo.attrib["quat"] = list_to_str(quat)
def AddHeighFieldFromImage(
self,
position=[1.0, 0.0, 0.0], # position
euler=[0.0, -0.0, 0.0], # attitude
size=[2.0, 1.6], # width and length
height_scale=0.02, # max height
negative_height=0.1, # height in the negative direction of z axis
input_img=None,
output_hfield_image="height_field.png",
image_scale=[1.0, 1.0], # reduce image resolution
invert_gray=False):
input_image = cv2.imread(input_img) # 替换为你的图像文件路径
width = int(input_image.shape[1] * image_scale[0])
height = int(input_image.shape[0] * image_scale[1])
resized_image = cv2.resize(input_image, (width, height),
interpolation=cv2.INTER_AREA)
terrain_image = cv2.cvtColor(resized_image, cv2.COLOR_BGR2GRAY)
if invert_gray:
terrain_image = 255 - position
cv2.imwrite(str(PATH_DIR / "assets" / output_hfield_image), terrain_image)
hfield = xml_et.SubElement(self.asset, "hfield")
hfield.attrib["name"] = "image_hfield"
hfield.attrib["size"] = list_to_str(
[size[0] / 2.0, size[1] / 2.0, height_scale, negative_height])
hfield.attrib["file"] = "../" + output_hfield_image
geo = xml_et.SubElement(self.worldbody, "geom")
geo.attrib["type"] = "hfield"
geo.attrib["hfield"] = "image_hfield"
geo.attrib["pos"] = list_to_str(position)
quat = euler_to_quat(euler[0], euler[1], euler[2])
geo.attrib["quat"] = list_to_str(quat)
def Save(self):
self.scene.write(OUTPUT_SCENE_PATH)
def AddSlope(self,
position=[1.0, 0.0, 0.0],
yaw=0.0,
length=0.575,
width=0.55,
height=0.15,
thickness=0,
add_baffle=True,
add_baffle_height=False,
sponge=False):
"""
生成带后背板和两侧挡板的斜坡。
侧挡板上沿与斜坡面对齐。
"""
if not thickness:
thickness = 0.05 # 斜坡面板厚度
side_thickness = 0.05 # 侧板厚度
# ===========================
# 1. 计算公共几何参数
# ===========================
# 计算坡度角和斜边长
angle = np.arctan2(height, length)
ramp_len = np.sqrt(length**2 + height**2)
# 法线向量 (nx, nz)
nx = np.sin(angle)
nz = -np.cos(angle)
# 斜坡面板的几何中心 (局部坐标)
# 这里的逻辑是将面板中心沿着法线向下偏移厚度的一半,保证上表面对齐理想斜面
mid_x = length / 2.0
mid_z = height / 2.0
lx = mid_x + nx * (thickness / 2.0)
lz = mid_z + nz * (thickness / 2.0)
# 统一的旋转角度 (Pitch: -angle, Yaw: yaw)
final_euler = [0.0, -angle, yaw]
# ===========================
# 2. 生成主斜坡面 (Ramp)
# ===========================
gx, gy = rot2d(lx, 0, yaw)
ramp_pos = [position[0] + gx, position[1] + gy, position[2] + lz]
self.AddBox(ramp_pos, final_euler, [ramp_len, width, thickness],sponge=sponge)
# ===========================
# 3. 生成垂直背板 (Back Wall)
# ===========================
# 位于斜坡末端,高度为 height
if add_baffle:
back_lx = length + (thickness / 2.0)
if add_baffle_height:
back_lz = height
height *= 2
else:
back_lz = height / 2.0
bgx, bgy = rot2d(back_lx, 0, yaw)
back_pos = [position[0] + bgx, position[1] + bgy, position[2] + back_lz]
# 背板竖直放置只受Yaw影响
self.AddBox(back_pos, [0, 0, yaw], [thickness, width, height])
# 生成两侧挡板 (Side Walls)
side_h = height / 2
# 计算侧板的中心 Z 坐标 (side_lz)
# 目标:侧板的上表面 Z = 斜坡的上表面 Z
# 斜坡上表面 Z (局部) = lz + thickness/2
# 侧板上表面 Z (局部) = side_lz + side_h/2
# 等式lz + thickness/2 = side_lz + side_h/2
# 解得:
side_lz = lz + (thickness / 2.0) - (side_h / 2.0)
if add_baffle_height:
side_lz += side_h
# side_h *= 2
# lx += height * 2 / length / 2
# 侧板的 X 坐标与斜坡中心一致 (lx)
# 侧板的 Y 偏移量
# 放在斜坡宽度的两侧:(斜坡宽/2) + (侧板厚/2)
y_shift = (width / 2.0) + (side_thickness / 2.0)
# 生成左右两个侧板
for sign in [-1, 1]: # -1:左侧, 1:右侧
if add_baffle_height and sign == 1: continue
local_y = sign * y_shift
# 将 (lx, local_y) 旋转 Yaw 角到全局
sgx, sgy = rot2d(lx, local_y, yaw)
side_pos = [
position[0] + sgx,
position[1] + sgy,
position[2] + side_lz
]
# 侧板的旋转角度与斜坡完全一致,这样上边缘才会平行
self.AddBox(side_pos, final_euler, [ramp_len, side_thickness, side_h])
def AddSlopeGroup(self, position=[0.0, 0.0, 0.0], yaw=0.0, add_baffle=True, add_baffle_height=False,):
L = 0.6 # 坡长 (爬升方向)
W = 0.6 # 坡宽 (侧向)
H = 0.164
p1_local = [-L / 2, -W]
yaw1 = np.pi / 2
p2_local = [-L, W/2]
yaw2 = 0
p3_local = [L, -W/2]
yaw3 = np.pi
p4_local = [L / 2, W]
yaw4 = np.pi * 3 / 2
blocks = [
(p1_local, yaw1),
(p2_local, yaw2),
(p3_local, yaw3),
(p4_local, yaw4)
]
for pos_local, local_yaw in blocks:
off_x, off_y = rot2d(pos_local[0], pos_local[1], yaw)
abs_pos = [
position[0] + off_x,
position[1] + off_y,
position[2]
]
abs_yaw = yaw + local_yaw
self.AddSlope(position=abs_pos,
yaw=abs_yaw,
length=L,
width=W,
height=H,
add_baffle=add_baffle,
add_baffle_height=add_baffle_height)
def AddBlockyHeightField(
self,
position=[1.0, 0.0, 0.0],
euler=[0.0, -0.0, 0.0],
size=[1.0, 1.0],
height_scale=0.2, # 高度差幅度
negative_height=0.1,
image_width=128,
img_height=128,
smooth=50.0, # 注意:如果要完全随机,把这个数改得很小(如 2.0
pixels_per_block=16, # <--- 新参数:决定方块的大小
output_hfield_image="height_field.png"):
# 1. 准备图像数据
terrain_image = np.zeros((img_height, image_width), dtype=np.uint8)
# 2. 预先生成一个随机种子偏移,保证每次地形不一样
seed_offset_x = np.random.randint(0, 10000)
seed_offset_y = np.random.randint(0, 10000)
for y in range(img_height):
for x in range(image_width):
# === 核心修改开始 ===
# 这里的整除逻辑 (//) 是制造“方块感”的关键
# 它将坐标强制归整,例如 x=0到15 都会变成 0x=16到31 都会变成 16
# 这样这 16 个像素取到的噪声值就是一模一样的,形成平坦的台阶
block_x = (x // pixels_per_block) * pixels_per_block
block_y = (y // pixels_per_block) * pixels_per_block
# 使用归整后的 block_x, block_y 来生成噪声
noise_value = noise.pnoise2((block_x + seed_offset_x) / smooth,
(block_y + seed_offset_y) / smooth,
octaves=1, # 减少细节,让方块表面平整
persistence=0.5,
lacunarity=2.0)
# === 核心修改结束 ===
# 映射到 0-255
terrain_image[y, x] = int((noise_value + 1) / 2 * 255)
hfield = xml_et.SubElement(self.asset, "hfield")
hfield.attrib["name"] = "perlin_hfield"
hfield.attrib["size"] = list_to_str(
[size[0] / 2.0, size[1] / 2.0, height_scale, negative_height])
hfield.attrib["file"] = output_hfield_image
geo = xml_et.SubElement(self.worldbody, "geom")
geo.attrib["type"] = "hfield"
geo.attrib["hfield"] = "perlin_hfield"
geo.attrib["pos"] = list_to_str(position)
quat = euler_to_quat(euler[0], euler[1], euler[2])
geo.attrib["quat"] = list_to_str(quat)
def unit1_sponge(self):
self.AddSlope(position=[0.0, 1.2, 0.0], yaw=np.pi, length=2.32, width=1.2, height=0.6, add_baffle_height=True)
self.AddSlope(position=[-2.32, 0.0, 0.0], yaw=0, length=2.32, width=1.2, height=0.6, add_baffle_height=True)
self.AddBox(position=[0.6, -0.6, 0.3], size=[1.2, 0.6, 0.03], euler=[np.pi / 2, 0, 0])
self.AddBox(position=[-2.92, -0.6, 0.3], size=[1.2, 0.6, 0.03], euler=[np.pi / 2, 0, 0])
self.AddBox(position=[0.6, 1.8, 0.3], size=[1.2, 0.6, 0.03], euler=[np.pi / 2, 0, 0])
self.AddBox(position=[-2.92, 1.8, 0.3], size=[1.2, 0.6, 0.03], euler=[np.pi / 2, 0, 0])
self.AddBox(position=[-3.52, 0.0, 0.3], size=[0.6, 1.2, 0.03], euler=[0, np.pi / 2, 0])
self.AddBox(position=[-2.92, 0.6, 0.025], size=[1.2, 2.4, 0.05], sponge=True)
self.AddBox(position=[0.6, 0.6, 0.025], size=[1.2, 2.4, 0.05], sponge=True)
self.AddSlope(position=[0.0, 1.2, 0.05], yaw=np.pi, length=2.32, width=1.2, thickness=0.05, height=0.6, sponge=True, add_baffle=False)
self.AddSlope(position=[-2.32, 0.0, 0.05], yaw=0, length=2.32, width=1.2, thickness=0.05, height=0.6, sponge=True, add_baffle=False)
def unit2_slopes(self):
self.AddBox(position=[1.225, 0.6, 0.0], size=[2.4, 0.05, 0.1], euler=[0, 0, np.pi / 2])
self.AddSlopeGroup(position=[1.85, 0.0, 0.0], yaw=0.0, add_baffle=True)
self.AddSlopeGroup(position=[1.85, 1.2, 0.0], yaw=0.0, add_baffle=True)
self.AddBox(position=[2.475, 0.6, 0.0], size=[2.4, 0.05, 0.1], euler=[0, 0, np.pi / 2])
self.AddSlopeGroup(position=[3.1, 0.0, 0.0], yaw=0.0, add_baffle=True)
self.AddSlopeGroup(position=[3.1, 1.2, 0.0], yaw=0.0, add_baffle=True)
self.AddSlopeGroup(position=[4.3, 0.0, 0.0], yaw=0.0, add_baffle=True)
self.AddSlopeGroup(position=[4.3, 1.2, 0.0], yaw=0.0, add_baffle=True)
self.AddBox(position=[3.7, 0.575, 0.0], size=[2.4, 0.05, 0.1])
self.AddBox(position=[3.7, 0.625, 0.0], size=[2.4, 0.05, 0.1])
self.AddBox(position=[4.925, 0.6, 0.0], size=[2.4, 0.05, 0.1], euler=[0, 0, np.pi / 2])
self.AddSlopeGroup(position=[5.55, 0.0, 0.0], yaw=0.0, add_baffle=True)
self.AddSlopeGroup(position=[5.55, 1.2, 0.0], yaw=0.0, add_baffle=True)
self.AddBox(position=[6.175, 0.6, 0.0], size=[2.4, 0.05, 0.1], euler=[0, 0, np.pi / 2])
self.AddBox(position=[1.9, 1.8, 0.3], size=[1.4, 0.6, 0.03], euler=[np.pi / 2, 0, 0])
self.AddBox(position=[3.2, 1.8, 0.3], size=[1.2, 0.6, 0.03], euler=[np.pi / 2, 0, 0])
self.AddBox(position=[4.4, 1.8, 0.3], size=[1.2, 0.6, 0.03], euler=[np.pi / 2, 0, 0])
self.AddBox(position=[5.6, 1.8, 0.3], size=[1.2, 0.6, 0.03], euler=[np.pi / 2, 0, 0])
self.AddBox(position=[1.9, -0.6, 0.3], size=[1.4, 0.6, 0.03], euler=[np.pi / 2, 0, 0])
self.AddBox(position=[3.2, -0.6, 0.3], size=[1.2, 0.6, 0.03], euler=[np.pi / 2, 0, 0])
self.AddBox(position=[4.4, -0.6, 0.3], size=[1.2, 0.6, 0.03], euler=[np.pi / 2, 0, 0])
self.AddBox(position=[5.6, -0.6, 0.3], size=[1.2, 0.6, 0.03], euler=[np.pi / 2, 0, 0])
self.AddBox(position=[1.2, 0.0, 0.3], size=[0.6, 1.2, 0.03], euler=[0, np.pi / 2, 0])
self.AddBox(position=[2.5, 1.2, 0.3], size=[0.6, 1.2, 0.03], euler=[0, np.pi / 2, 0])
self.AddBox(position=[4.95, 0.0, 0.3], size=[0.6, 1.2, 0.03], euler=[0, np.pi / 2, 0])
self.AddBox(position=[6.2, 1.2, 0.3], size=[0.6, 1.2, 0.03], euler=[0, np.pi / 2, 0])
def unit3_stairs(self):
self.AddBox(position = [6.8, 1.2, 0.1], size = [1.2, 1.2, 0.2])
self.AddBox(position = [8.0, 0.0, 0.1], size = [1.2, 1.2, 0.2])
self.AddBox(position = [9.2, 1.2, 0.1], size = [1.2, 1.2, 0.2])
self.AddBox(position = [10.4, 0.0, 0.1], size = [1.2, 1.2, 0.2])
self.AddBox(position=[6.8, 1.8, 0.3], size=[1.2, 0.6, 0.03], euler=[np.pi / 2, 0, 0])
self.AddBox(position=[8.0, 1.8, 0.3], size=[1.2, 0.6, 0.03], euler=[np.pi / 2, 0, 0])
self.AddBox(position=[9.2, 1.8, 0.3], size=[1.2, 0.6, 0.03], euler=[np.pi / 2, 0, 0])
self.AddBox(position=[10.4, 1.8, 0.3], size=[1.2, 0.6, 0.03], euler=[np.pi / 2, 0, 0])
self.AddBox(position=[6.8, -0.6, 0.3], size=[1.2, 0.6, 0.03], euler=[np.pi / 2, 0, 0])
self.AddBox(position=[8.0, -0.6, 0.3], size=[1.2, 0.6, 0.03], euler=[np.pi / 2, 0, 0])
self.AddBox(position=[9.2, -0.6, 0.3], size=[1.2, 0.6, 0.03], euler=[np.pi / 2, 0, 0])
self.AddBox(position=[10.4, -0.6, 0.3], size=[1.2, 0.6, 0.03], euler=[np.pi / 2, 0, 0])
self.AddBox(position=[11.0, 1.2, 0.3], size=[0.6, 1.2, 0.03], euler=[0, np.pi / 2, 0])
def unit4_diagonal(self):
self.AddSlope(position=[12.8, 1.8, 0.0], yaw=np.pi / 2, length=2.32, width=1.2, height=0.6, add_baffle_height=True)
self.AddSlope(position=[12.8, 1.8, 0.05], yaw=np.pi / 2, length=2.32/48, width=1.2, height=0.6/48, thickness=0.05, add_baffle=False)
self.AddSlope(position=[12.8, 4.07, 0.6375], yaw=np.pi / 2, length=2.32/48, width=1.2, height=0.6/48, thickness=0.05, add_baffle=False)
self.AddSlope(position=[12.225, 1.8, 0.05], yaw=np.pi / 2, length=2.32, width=0.05, height=0.6, thickness=0.05, add_baffle=False)
self.AddSlope(position=[13.375, 1.8, 0.05], yaw=np.pi / 2, length=2.32, width=0.05, height=0.6, thickness=0.05, add_baffle=False)
self.AddBox(position=[12.8, 3.54, 0.475], size=[1.64, 0.05, 0.05], euler=[0.0, -0.1779, 0.7685])
self.AddBox(position=[12.8, 2.38, 0.175], size=[1.64, 0.05, 0.05], euler=[0.0, 0.1779, -0.7685])
self.AddSlope(position=[11.6, 4.12, 0.0], yaw=-np.pi / 2, length=2.32, width=1.2, height=0.6, add_baffle_height=True)
self.AddSlope(position=[11.6, 4.12, 0.05], yaw=-np.pi / 2, length=2.32/48, width=1.2, height=0.6/48, thickness=0.05, add_baffle=False)
self.AddSlope(position=[11.6, 1.85, 0.6375], yaw=-np.pi / 2, length=2.32/48, width=1.2, height=0.6/48, thickness=0.05, add_baffle=False)
self.AddSlope(position=[12.175, 4.12, 0.05], yaw=-np.pi / 2, length=2.32, width=0.05, height=0.6, thickness=0.05, add_baffle=False)
self.AddSlope(position=[11.025, 4.12, 0.05], yaw=-np.pi / 2, length=2.32, width=0.05, height=0.6, thickness=0.05, add_baffle=False)
self.AddBox(position=[11.6, 3.54, 0.175], size=[1.64, 0.05, 0.05], euler=[0.0, -0.1779, -0.7685])
self.AddBox(position=[11.6, 2.38, 0.475], size=[1.64, 0.05, 0.05], euler=[0.0, 0.1779, 0.7685])
self.AddBox(position=[11.6, -0.6, 0.3], size=[1.2, 0.6, 0.03], euler=[np.pi / 2, 0, 0])
self.AddBox(position=[12.2, 0.0, 0.3], size=[0.6, 1.2, 0.03], euler=[0, np.pi / 2, 0])
self.AddBox(position=[12.8, 0.6, 0.3], size=[1.2, 0.6, 0.03], euler=[np.pi / 2, 0, 0])
self.AddBox(position=[13.4, 1.2, 0.3], size=[0.6, 1.2, 0.03], euler=[0, np.pi / 2, 0])
self.AddBox(position=[12.2, 0.625, 0.025], size=[2.4, 0.05, 0.05])
self.AddBox(position=[12.2, 1.775, 0.025], size=[2.4, 0.05, 0.05])
self.AddBox(position=[11.025, 1.2, 0.025], size=[1.2, 0.05, 0.05], euler=[0, 0, np.pi / 2])
self.AddBox(position=[13.375, 1.2, 0.025], size=[1.2, 0.05, 0.05], euler=[0, 0, np.pi / 2])
self.AddBox(position=[11.6, 1.2, 0.025], size=[1.64, 0.05, 0.05], euler=[0, 0, -np.pi / 4])
self.AddBox(position=[12.8, 1.2, 0.025], size=[1.64, 0.05, 0.05], euler=[0, 0, np.pi / 4])
self.AddBox(position=[12.2, 4.145, 0.025], size=[2.4, 0.05, 0.05])
self.AddBox(position=[12.2, 5.295, 0.025], size=[2.4, 0.05, 0.05])
self.AddBox(position=[11.025, 4.72, 0.025], size=[1.2, 0.05, 0.05], euler=[0, 0, np.pi / 2])
self.AddBox(position=[13.375, 4.72, 0.025], size=[1.2, 0.05, 0.05], euler=[0, 0, np.pi / 2])
self.AddBox(position=[11.6, 4.72, 0.025], size=[1.64, 0.05, 0.05], euler=[0, 0, np.pi / 4])
self.AddBox(position=[12.8, 4.72, 0.025], size=[1.64, 0.05, 0.05], euler=[0, 0, -np.pi / 4])
self.AddBox(position=[11.0, 4.72, 0.3], size=[0.6, 1.2, 0.03], euler=[0, np.pi / 2, 0])
self.AddBox(position=[13.4, 4.72, 0.3], size=[0.6, 1.2, 0.03], euler=[0, np.pi / 2, 0])
self.AddBox(position=[11.6, 5.32, 0.3], size=[1.2, 0.6, 0.03], euler=[np.pi / 2, 0, 0])
self.AddBox(position=[11.6, 0.0, 0.0], size=[1.2, 1.2, 0.03])
self.AddBox(position=[12.2, 1.2, 0.0], size=[2.4, 1.2, 0.03])
self.AddBox(position=[12.2, 4.72, 0.0], size=[2.4, 1.2, 0.03])
def unit5_sandstone(self):
self.AddBox(position=[11.0, 5.92, 0.3], size=[0.6, 1.2, 0.03], euler=[0, np.pi / 2, 0])
self.AddBox(position=[11.0, 7.12, 0.3], size=[0.6, 1.2, 0.03], euler=[0, np.pi / 2, 0])
self.AddBox(position=[11.0, 8.32, 0.3], size=[0.6, 1.2, 0.03], euler=[0, np.pi / 2, 0])
self.AddBox(position=[11.0, 9.52, 0.3], size=[0.6, 1.2, 0.03], euler=[0, np.pi / 2, 0])
self.AddBox(position=[13.4, 5.92, 0.3], size=[0.6, 1.2, 0.03], euler=[0, np.pi / 2, 0])
self.AddBox(position=[13.4, 7.12, 0.3], size=[0.6, 1.2, 0.03], euler=[0, np.pi / 2, 0])
self.AddBox(position=[13.4, 8.32, 0.3], size=[0.6, 1.2, 0.03], euler=[0, np.pi / 2, 0])
self.AddBox(position=[13.4, 9.52, 0.3], size=[0.6, 1.2, 0.03], euler=[0, np.pi / 2, 0])
self.AddBox(position=[12.8, 6.52, 0.3], size=[1.2, 0.6, 0.03], euler=[np.pi / 2, 0, 0])
self.AddBox(position=[12.8, 10.12, 0.3], size=[1.2, 0.6, 0.03], euler=[np.pi / 2, 0, 0])
self.AddBox(position=[11.6, 8.92, 0.3], size=[1.2, 0.6, 0.03], euler=[np.pi / 2, 0, 0])
self.AddBox(position=[12.2, 6.545, 0.05], size=[2.4, 0.05, 0.05])
self.AddBox(position=[12.2, 8.895, 0.05], size=[2.4, 0.05, 0.05])
self.AddBox(position=[11.025, 7.72, 0.05], size=[2.4, 0.05, 0.05], euler=[0, 0, np.pi / 2])
self.AddBox(position=[13.375, 7.72, 0.05], size=[2.4, 0.05, 0.05], euler=[0, 0, np.pi / 2])
self.AddBox(position=[12.2, 7.72, 0.05], size=[3.28, 0.05, 0.05], euler=[0, 0, -np.pi / 4])
self.AddBox(position=[12.2, 7.72, 0.05], size=[3.28, 0.05, 0.05], euler=[0, 0, np.pi / 4])
self.AddBox(position=[11.6, 7.12, 0.05], size=[1.64, 0.05, 0.05], euler=[0, 0, -np.pi / 4])
self.AddBox(position=[12.8, 7.12, 0.05], size=[1.64, 0.05, 0.05], euler=[0, 0, np.pi / 4])
self.AddBox(position=[11.6, 8.32, 0.05], size=[1.64, 0.05, 0.05], euler=[0, 0, np.pi / 4])
self.AddBox(position=[12.8, 8.32, 0.05], size=[1.64, 0.05, 0.05], euler=[0, 0, -np.pi / 4])
self.AddBlockyHeightField(position=[12.2, 7.72, -0.00], size=[2.4, 4.8], height_scale=0.08)
if __name__ == "__main__":
tg = TerrainGenerator()
# # Box obstacle
# tg.AddBox(position=[0.55 / 2 + 2, 0, 0.075], size=[np.sqrt(0.55 * 0.55 + 0.15*0.15), 0.1, 0.01], euler=[np.pi / 2, 0, 0])
# tg.AddBox(position=[3.5, 0, 0.075], size=[1.5, 0.5, 0.01], euler=[0, 0, 0])
# # Geometry obstacle
# # geo_type supports "plane", "sphere", "capsule", "ellipsoid", "cylinder", "box"
# tg.AddGeometry(position=[1.5, 0.0, 0.25], euler=[0, 0, 0.0], size=[1.0,0.5,0.5],geo_type="cylinder")
# # Slope
# tg.AddBox(position=[2.0, 2.0, 0.5],
# euler=[0.0, -0.5, 0.0],
# size=[3, 1.5, 0.1])
# # Stairs
# tg.AddStairs(init_pos=[1.0, 4.0, 0.0], yaw=0.0)
# # Suspend stairs
# tg.AddSuspendStairs(init_pos=[1.0, 6.0, 0.0], yaw=0.0)
# # Rough ground
# tg.AddRoughGround(init_pos=[-2.5, 5.0, 0.0],
# euler=[0, 0, 0.0],
# nums=[10, 8])
# # Perlin heigh field
# tg.AddPerlinHeighField(position=[-1.5, 4.0, 0.0], size=[2.0, 1.5])
# # Heigh field from image
# tg.AddHeighFieldFromImage(position=[-1.5, 2.0, 0.0],
# euler=[0, 0, -1.57],
# size=[2.0,2.0],
# input_img="./unitree_robot.jpeg",
# image_scale=[1.0, 1.0],
# output_hfield_image="unitree_hfield.png")
tg.unit1_sponge()
tg.unit2_slopes()
tg.unit3_stairs()
tg.unit4_diagonal()
tg.unit5_sandstone()
tg.Save()

File diff suppressed because it is too large Load Diff

12
rsl_rl/.gitignore vendored Normal file
View File

@@ -0,0 +1,12 @@
# IDEs
.idea
# builds
*.egg-info
# cache
__pycache__
.pytest_cache
# vs code
.vscode

30
rsl_rl/LICENSE Normal file
View File

@@ -0,0 +1,30 @@
Copyright (c) 2021, ETH Zurich, Nikita Rudin
Copyright (c) 2021, NVIDIA CORPORATION & AFFILIATES
All rights reserved.
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.
See licenses/dependencies for license information of dependencies of this package.

26
rsl_rl/README.md Normal file
View File

@@ -0,0 +1,26 @@
# RSL RL
Fast and simple implementation of RL algorithms, designed to run fully on GPU.
This code is an evolution of `rl-pytorch` provided with NVIDIA's Isaac GYM.
Only PPO is implemented for now. More algorithms will be added later.
Contributions are welcome.
## Setup
```
git clone https://github.com/leggedrobotics/rsl_rl
cd rsl_rl
pip install -e .
```
### Useful Links ###
Example use case: https://github.com/leggedrobotics/legged_gym
Project website: https://leggedrobotics.github.io/legged_gym/
Paper: https://arxiv.org/abs/2109.11978
**Maintainer**: Nikita Rudin
**Affiliation**: Robotic Systems Lab, ETH Zurich & NVIDIA
**Contact**: rudinn@ethz.ch

View File

@@ -0,0 +1,30 @@
Copyright (c) 2005-2021, NumPy Developers.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* 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.
* Neither the name of the NumPy Developers nor the names of any
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
OWNER 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.

View File

@@ -0,0 +1,73 @@
From PyTorch:
Copyright (c) 2016- Facebook, Inc (Adam Paszke)
Copyright (c) 2014- Facebook, Inc (Soumith Chintala)
Copyright (c) 2011-2014 Idiap Research Institute (Ronan Collobert)
Copyright (c) 2012-2014 Deepmind Technologies (Koray Kavukcuoglu)
Copyright (c) 2011-2012 NEC Laboratories America (Koray Kavukcuoglu)
Copyright (c) 2011-2013 NYU (Clement Farabet)
Copyright (c) 2006-2010 NEC Laboratories America (Ronan Collobert, Leon Bottou, Iain Melvin, Jason Weston)
Copyright (c) 2006 Idiap Research Institute (Samy Bengio)
Copyright (c) 2001-2004 Idiap Research Institute (Ronan Collobert, Samy Bengio, Johnny Mariethoz)
From Caffe2:
Copyright (c) 2016-present, Facebook Inc. All rights reserved.
All contributions by Facebook:
Copyright (c) 2016 Facebook Inc.
All contributions by Google:
Copyright (c) 2015 Google Inc.
All rights reserved.
All contributions by Yangqing Jia:
Copyright (c) 2015 Yangqing Jia
All rights reserved.
All contributions by Kakao Brain:
Copyright 2019-2020 Kakao Brain
All contributions from Caffe:
Copyright(c) 2013, 2014, 2015, the respective contributors
All rights reserved.
All other contributions:
Copyright(c) 2015, 2016 the respective contributors
All rights reserved.
Caffe2 uses a copyright model similar to Caffe: each contributor holds
copyright over their contributions to Caffe2. The project versioning records
all such contribution and copyright details. If a contributor wants to further
mark their specific copyright on a particular contribution, they should
indicate their copyright solely in the commit message of the change when it is
committed.
All rights reserved.
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 names of Facebook, Deepmind Technologies, NYU, NEC Laboratories America
and IDIAP Research Institute 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 OWNER 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.

29
rsl_rl/rsl_rl/__init__.py Normal file
View File

@@ -0,0 +1,29 @@
# SPDX-FileCopyrightText: Copyright (c) 2021 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: BSD-3-Clause
#
# 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.
#
# Copyright (c) 2021 ETH Zurich, Nikita Rudin

View File

@@ -0,0 +1,33 @@
# SPDX-FileCopyrightText: Copyright (c) 2021 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: BSD-3-Clause
#
# 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.
#
# Copyright (c) 2021 ETH Zurich, Nikita Rudin
from .ppo import PPO
from .cts import CTS
from .moe_cts import MoECTS

View File

@@ -0,0 +1,286 @@
# SPDX-FileCopyrightText: Copyright (c) 2021 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: BSD-3-Clause
#
# 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.
#
# Copyright (c) 2021 ETH Zurich, Nikita Rudin
import torch
import torch.nn as nn
import torch.optim as optim
import itertools
from rsl_rl.modules import ActorCriticCTS
from rsl_rl.storage import RolloutStorageCTS
class CTS:
model: ActorCriticCTS
def __init__(self,
model,
num_envs,
history_length,
num_learning_epochs=1,
num_mini_batches=1,
clip_param=0.2,
gamma=0.998,
lam=0.95,
value_loss_coef=1.0,
entropy_coef=0.0,
learning_rate=1e-3,
student_encoder_learning_rate=1e-3,
max_grad_norm=1.0,
use_clipped_value_loss=True,
schedule="fixed",
desired_kl=0.01,
teacher_env_ratio=0.75,
device='cpu',
):
self.device = device
self.desired_kl = desired_kl
self.schedule = schedule
self.learning_rate = learning_rate
self.history_length = history_length
# CTS components
self.model = model
self.model.to(self.device)
self.storage = None # initialized later
params1 = [
{"params": self.model.teacher_encoder.parameters()},
{"params": self.model.critic.parameters()},
{"params": self.model.actor.parameters()},
{"params": self.model.std}
]
self.optimizer1 = optim.Adam(params1, lr=learning_rate)
self.optimizer2 = optim.Adam(self.model.student_encoder.parameters(), lr=student_encoder_learning_rate)
self.transition = RolloutStorageCTS.Transition()
# CTS parameters
self.clip_param = clip_param
self.num_learning_epochs = num_learning_epochs
self.num_mini_batches = num_mini_batches
self.value_loss_coef = value_loss_coef
self.entropy_coef = entropy_coef
self.gamma = gamma
self.lam = lam
self.max_grad_norm = max_grad_norm
self.use_clipped_value_loss = use_clipped_value_loss
self.teacher_num_envs = max(int(num_envs * teacher_env_ratio), 1)
self.student_num_envs = num_envs - self.teacher_num_envs
student_env_ratio = 1 - teacher_env_ratio
self.teacher_env_idxs = torch.tensor([i for i in range(num_envs) if i % int(1/student_env_ratio) != 0], device=self.device)
self.student_env_idxs = torch.tensor([i for i in range(num_envs) if i % int(1/student_env_ratio) == 0], device=self.device)
# self.teacher_env_idxs = torch.arange(num_envs, device=self.device)
# self.student_env_idxs = []
assert len(self.teacher_env_idxs) == self.teacher_num_envs, f"{len(self.teacher_env_idxs)=} != {self.teacher_num_envs=}"
assert len(self.student_env_idxs) == self.student_num_envs, f"{len(self.student_env_idxs)=} != {self.student_num_envs=}"
def init_storage(self, num_envs, num_transitions_per_env, actor_obs_shape, critic_obs_shape, action_shape):
self.storage = RolloutStorageCTS(num_envs, self.teacher_num_envs, self.history_length, num_transitions_per_env, actor_obs_shape, critic_obs_shape, action_shape, self.device)
def test_mode(self):
self.model.test()
def train_mode(self):
self.model.train()
def act(self, obs, privileged_obs, history):
history = history.clone()
def get_results(obs, privileged_obs, history, is_teacher):
actions = self.model.act(obs, privileged_obs, history, is_teacher).detach()
return (
actions,
self.model.evaluate(privileged_obs, history, is_teacher).detach(),
self.model.get_actions_log_prob(actions).detach(),
self.model.action_mean.detach(),
self.model.action_std.detach(),
)
ti, si = self.teacher_env_idxs, self.student_env_idxs
teacher_results = get_results(obs[ti], privileged_obs[ti], history[ti], True)
student_results = get_results(obs[si], privileged_obs[si], history[si], False)
results = []
for x1, x2 in zip(teacher_results, student_results):
results.append(torch.cat([x1, x2], dim=0))
# Compute the actions and values
self.transition.actions = results[0]
self.transition.values = results[1]
self.transition.actions_log_prob = results[2]
self.transition.action_mean = results[3]
self.transition.action_sigma = results[4]
# need to record obs and critic_obs before env.step()
self.transition.history = torch.cat([history[ti], history[si]], dim=0)
self.transition.observations = torch.cat([obs[ti], obs[si]], dim=0)
self.transition.critic_observations = torch.cat([privileged_obs[ti], privileged_obs[si]], dim=0)
real_actions = torch.zeros_like(self.transition.actions)
real_actions[ti] = self.transition.actions[:self.teacher_num_envs]
real_actions[si] = self.transition.actions[self.teacher_num_envs:]
return real_actions
def process_env_step(self, rewards, dones, infos):
ti, si = self.teacher_env_idxs, self.student_env_idxs
rewards = rewards.clone()
self.transition.rewards = torch.cat([rewards[ti], rewards[si]], dim=0)
self.transition.dones = torch.cat([dones[ti], dones[si]], dim=0)
# Bootstrapping on time outs
if 'time_outs' in infos:
time_outs = torch.cat([infos['time_outs'][ti], infos['time_outs'][si]], dim=0)
self.transition.rewards += self.gamma * torch.squeeze(self.transition.values * time_outs.unsqueeze(1).to(self.device), 1)
# Record the transition
self.storage.add_transitions(self.transition)
self.transition.clear()
self.model.reset(dones)
def compute_returns(self, last_privileged_obs, last_history):
ti, si = self.teacher_env_idxs, self.student_env_idxs
last_values = torch.cat([
self.model.evaluate(last_privileged_obs[ti], last_history[ti], True).detach(),
self.model.evaluate(last_privileged_obs[si], last_history[si], False).detach(),
], dim=0)
self.storage.compute_returns(last_values, self.gamma, self.lam)
def update(self):
mean_value_loss = 0
mean_surrogate_loss = 0
mean_entropy_loss = 0
mean_latent_loss = 0
assert not self.model.is_recurrent
data = list(self.storage.mini_batch_generator(self.num_mini_batches, self.num_learning_epochs))
teacher_samples = self.teacher_num_envs * self.storage.num_transitions_per_env // self.num_mini_batches
student_samples = self.student_num_envs * self.storage.num_transitions_per_env // self.num_mini_batches
for sample in data:
(
obs_batch, privileged_obs_batch, actions_batch, history_batch,
target_values_batch, advantages_batch, returns_batch,
old_actions_log_prob_batch, old_mu_batch, old_sigma_batch,
hid_states_batch, masks_batch
) = sample
def get_results(start, end, is_teacher):
self.model.act(obs_batch[start:end], privileged_obs_batch[start:end], history_batch[start:end], is_teacher)
actions_log_prob = self.model.get_actions_log_prob(actions_batch[start:end])
value = self.model.evaluate(privileged_obs_batch[start:end], history_batch[start:end], is_teacher)
mu = self.model.action_mean
sigma = self.model.action_std
entropy = self.model.entropy
return actions_log_prob, value, mu, sigma, entropy
teacher_results = get_results(0, teacher_samples, True)
student_results = get_results(teacher_samples, teacher_samples + student_samples, False)
results = []
for x1, x2 in zip(teacher_results, student_results):
results.append(torch.cat([x1, x2], dim=0))
actions_log_prob_batch = results[0]
value_batch = results[1]
mu_batch = results[2]
sigma_batch = results[3]
entropy_batch = results[4]
# KL
if self.desired_kl != None and self.schedule == 'adaptive':
with torch.inference_mode():
kl = torch.sum(
torch.log(
sigma_batch / old_sigma_batch + 1.e-5) + (
torch.square(old_sigma_batch) +
torch.square(old_mu_batch - mu_batch)
) / (2.0 * torch.square(sigma_batch)) - 0.5, axis=-1)
kl_mean = torch.mean(kl)
if kl_mean > self.desired_kl * 2.0:
self.learning_rate = max(1e-5, self.learning_rate / 1.5)
elif kl_mean < self.desired_kl / 2.0 and kl_mean > 0.0:
self.learning_rate = min(1e-2, self.learning_rate * 1.5)
for param_group in self.optimizer1.param_groups:
param_group['lr'] = self.learning_rate
# Surrogate loss
ratio = torch.exp(actions_log_prob_batch - torch.squeeze(old_actions_log_prob_batch))
surrogate = -torch.squeeze(advantages_batch) * ratio
surrogate_clipped = -torch.squeeze(advantages_batch) * torch.clamp(ratio, 1.0 - self.clip_param,
1.0 + self.clip_param)
surrogate_losses = torch.max(surrogate, surrogate_clipped)
teacher_surrogate_loss = surrogate_losses[:teacher_samples].mean()
student_surrogate_loss = surrogate_losses[teacher_samples:].mean()
surrogate_loss = teacher_surrogate_loss + student_surrogate_loss
# surrogate_loss = teacher_surrogate_loss
# Value function loss
if self.use_clipped_value_loss:
value_clipped = target_values_batch + (value_batch - target_values_batch).clamp(-self.clip_param,
self.clip_param)
value_losses = (value_batch - returns_batch).pow(2)
value_losses_clipped = (value_clipped - returns_batch).pow(2)
value_loss = torch.max(value_losses, value_losses_clipped).mean()
else:
value_loss = (returns_batch - value_batch).pow(2).mean()
# teacher_value_loss = value_losses[:teacher_samples].mean()
# student_value_loss = value_losses[teacher_samples:].mean()
# value_loss = teacher_value_loss # + student_value_loss
loss = surrogate_loss + self.value_loss_coef * value_loss - self.entropy_coef * entropy_batch.mean()
# Gradient step
self.optimizer1.zero_grad()
loss.backward()
params_to_clip = itertools.chain.from_iterable(g['params'] for g in self.optimizer1.param_groups)
nn.utils.clip_grad_norm_(params_to_clip, self.max_grad_norm)
self.optimizer1.step()
mean_value_loss += value_loss.item()
mean_surrogate_loss += surrogate_loss.item()
mean_entropy_loss += entropy_batch.mean().item()
for sample in data:
(
obs_batch, privileged_obs_batch, actions_batch, history_batch,
target_values_batch, advantages_batch, returns_batch,
old_actions_log_prob_batch, old_mu_batch, old_sigma_batch,
hid_states_batch, masks_batch
) = sample
# Student encoder update
student_latent = self.model.student_encoder(history_batch[teacher_samples:])
with torch.no_grad():
teacher_latent = self.model.teacher_encoder(privileged_obs_batch[teacher_samples:])
latent_loss = (teacher_latent - student_latent).pow(2).mean()
self.optimizer2.zero_grad()
latent_loss.backward()
nn.utils.clip_grad_norm_(self.model.student_encoder.parameters(), self.max_grad_norm)
self.optimizer2.step()
mean_latent_loss += latent_loss.item()
num_updates = self.num_learning_epochs * self.num_mini_batches
mean_value_loss /= num_updates
mean_surrogate_loss /= num_updates
mean_entropy_loss /= num_updates
mean_latent_loss /= num_updates
self.storage.clear()
return mean_value_loss, mean_surrogate_loss, mean_entropy_loss, mean_latent_loss

View File

@@ -0,0 +1,234 @@
# SPDX-FileCopyrightText: Copyright (c) 2021 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: BSD-3-Clause
#
# 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.
#
# Copyright (c) 2021 ETH Zurich, Nikita Rudin
import torch
import torch.nn as nn
import torch.optim as optim
import itertools
from rsl_rl.modules import ActorCriticMoECTS
from rsl_rl.storage import RolloutStorageCTS
from rsl_rl.algorithms.cts import CTS
class MoECTS(CTS):
model: ActorCriticMoECTS
def __init__(self,
model,
num_envs,
history_length,
num_learning_epochs=1,
num_mini_batches=1,
clip_param=0.2,
gamma=0.998,
lam=0.95,
value_loss_coef=1.0,
entropy_coef=0.0,
load_balance_coef=0.01,
learning_rate=1e-3,
student_encoder_learning_rate=1e-3,
max_grad_norm=1.0,
use_clipped_value_loss=True,
schedule="fixed",
desired_kl=0.01,
teacher_env_ratio=0.75,
device='cpu',
):
self.device = device
self.desired_kl = desired_kl
self.schedule = schedule
self.learning_rate = learning_rate
self.history_length = history_length
# CTS components
self.model = model
self.model.to(self.device)
self.storage = None # initialized later
params1 = [
{"params": self.model.teacher_encoder.parameters()},
{"params": self.model.critic.parameters()},
{"params": self.model.actor.parameters()},
{"params": self.model.std}
]
self.optimizer1 = optim.Adam(params1, lr=learning_rate)
self.optimizer2 = optim.Adam(self.model.student_moe_encoder.parameters(), lr=student_encoder_learning_rate)
self.transition = RolloutStorageCTS.Transition()
# CTS parameters
self.clip_param = clip_param
self.num_learning_epochs = num_learning_epochs
self.num_mini_batches = num_mini_batches
self.value_loss_coef = value_loss_coef
self.entropy_coef = entropy_coef
self.load_balance_coef = load_balance_coef
self.gamma = gamma
self.lam = lam
self.max_grad_norm = max_grad_norm
self.use_clipped_value_loss = use_clipped_value_loss
self.teacher_num_envs = max(int(num_envs * teacher_env_ratio), 1)
self.student_num_envs = num_envs - self.teacher_num_envs
student_env_ratio = 1 - teacher_env_ratio
self.teacher_env_idxs = torch.tensor([i for i in range(num_envs) if i % int(1/student_env_ratio) != 0], device=self.device)
self.student_env_idxs = torch.tensor([i for i in range(num_envs) if i % int(1/student_env_ratio) == 0], device=self.device)
assert len(self.teacher_env_idxs) == self.teacher_num_envs, f"{len(self.teacher_env_idxs)=} != {self.teacher_num_envs=}"
assert len(self.student_env_idxs) == self.student_num_envs, f"{len(self.student_env_idxs)=} != {self.student_num_envs=}"
def update(self):
mean_value_loss = 0
mean_surrogate_loss = 0
mean_entropy_loss = 0
mean_latent_loss = 0
mean_load_balance_loss = 0
assert not self.model.is_recurrent
data = list(self.storage.mini_batch_generator(self.num_mini_batches, self.num_learning_epochs))
teacher_samples = self.teacher_num_envs * self.storage.num_transitions_per_env // self.num_mini_batches
student_samples = self.student_num_envs * self.storage.num_transitions_per_env // self.num_mini_batches
for sample in data:
(
obs_batch, privileged_obs_batch, actions_batch, history_batch,
target_values_batch, advantages_batch, returns_batch,
old_actions_log_prob_batch, old_mu_batch, old_sigma_batch,
hid_states_batch, masks_batch
) = sample
def get_results(start, end, is_teacher):
self.model.act(obs_batch[start:end], privileged_obs_batch[start:end], history_batch[start:end], is_teacher)
actions_log_prob = self.model.get_actions_log_prob(actions_batch[start:end])
value = self.model.evaluate(privileged_obs_batch[start:end], history_batch[start:end], is_teacher)
mu = self.model.action_mean
sigma = self.model.action_std
entropy = self.model.entropy
return actions_log_prob, value, mu, sigma, entropy
teacher_results = get_results(0, teacher_samples, True)
student_results = get_results(teacher_samples, teacher_samples + student_samples, False)
results = []
for x1, x2 in zip(teacher_results, student_results):
results.append(torch.cat([x1, x2], dim=0))
actions_log_prob_batch = results[0]
value_batch = results[1]
mu_batch = results[2]
sigma_batch = results[3]
entropy_batch = results[4]
# KL
if self.desired_kl != None and self.schedule == 'adaptive':
with torch.inference_mode():
kl = torch.sum(
torch.log(
sigma_batch / old_sigma_batch + 1.e-5) + (
torch.square(old_sigma_batch) +
torch.square(old_mu_batch - mu_batch)
) / (2.0 * torch.square(sigma_batch)) - 0.5, axis=-1)
kl_mean = torch.mean(kl)
if kl_mean > self.desired_kl * 2.0:
self.learning_rate = max(1e-5, self.learning_rate / 1.5)
elif kl_mean < self.desired_kl / 2.0 and kl_mean > 0.0:
self.learning_rate = min(1e-2, self.learning_rate * 1.5)
for param_group in self.optimizer1.param_groups:
param_group['lr'] = self.learning_rate
# Surrogate loss
ratio = torch.exp(actions_log_prob_batch - torch.squeeze(old_actions_log_prob_batch))
surrogate = -torch.squeeze(advantages_batch) * ratio
surrogate_clipped = -torch.squeeze(advantages_batch) * torch.clamp(ratio, 1.0 - self.clip_param,
1.0 + self.clip_param)
surrogate_losses = torch.max(surrogate, surrogate_clipped)
teacher_surrogate_loss = surrogate_losses[:teacher_samples].mean()
student_surrogate_loss = surrogate_losses[teacher_samples:].mean()
surrogate_loss = teacher_surrogate_loss + student_surrogate_loss
# surrogate_loss = teacher_surrogate_loss
# Value function loss
if self.use_clipped_value_loss:
value_clipped = target_values_batch + (value_batch - target_values_batch).clamp(-self.clip_param,
self.clip_param)
value_losses = (value_batch - returns_batch).pow(2)
value_losses_clipped = (value_clipped - returns_batch).pow(2)
value_loss = torch.max(value_losses, value_losses_clipped).mean()
else:
value_loss = (returns_batch - value_batch).pow(2).mean()
# teacher_value_loss = value_losses[:teacher_samples].mean()
# student_value_loss = value_losses[teacher_samples:].mean()
# value_loss = teacher_value_loss # + student_value_loss
loss = surrogate_loss + self.value_loss_coef * value_loss - self.entropy_coef * entropy_batch.mean()
# Gradient step
self.optimizer1.zero_grad()
loss.backward()
params_to_clip = itertools.chain.from_iterable(g['params'] for g in self.optimizer1.param_groups)
nn.utils.clip_grad_norm_(params_to_clip, self.max_grad_norm)
self.optimizer1.step()
mean_value_loss += value_loss.item()
mean_surrogate_loss += surrogate_loss.item()
mean_entropy_loss += entropy_batch.mean().item()
for sample in data:
(
obs_batch, privileged_obs_batch, actions_batch, history_batch,
target_values_batch, advantages_batch, returns_batch,
old_actions_log_prob_batch, old_mu_batch, old_sigma_batch,
hid_states_batch, masks_batch
) = sample
# Student encoder update
student_latent, gating_weights = self.model.get_student_latent_and_weights(history_batch[teacher_samples:])
with torch.no_grad():
teacher_latent = self.model.teacher_encoder(privileged_obs_batch[teacher_samples:])
latent_loss = (teacher_latent - student_latent).pow(2).mean()
# Load balance loss
mean_usage = torch.mean(gating_weights, dim=0)
target_usage = torch.full_like(mean_usage, 1.0 / gating_weights.shape[1])
load_balance_loss = torch.mean((mean_usage - target_usage).pow(2))
# load_balance_loss = torch.sum(mean_usage.pow(2)) * gating_weights.shape[1] # Switch Transformer style
student_loss = latent_loss + self.load_balance_coef * load_balance_loss
self.optimizer2.zero_grad()
student_loss.backward()
nn.utils.clip_grad_norm_(self.model.student_moe_encoder.parameters(), self.max_grad_norm)
self.optimizer2.step()
mean_latent_loss += latent_loss.item()
mean_load_balance_loss += load_balance_loss.item()
num_updates = self.num_learning_epochs * self.num_mini_batches
mean_value_loss /= num_updates
mean_surrogate_loss /= num_updates
mean_entropy_loss /= num_updates
mean_latent_loss /= num_updates
mean_load_balance_loss /= num_updates
self.storage.clear()
return mean_value_loss, mean_surrogate_loss, mean_entropy_loss, mean_latent_loss, mean_load_balance_loss

View File

@@ -0,0 +1,187 @@
# SPDX-FileCopyrightText: Copyright (c) 2021 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: BSD-3-Clause
#
# 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.
#
# Copyright (c) 2021 ETH Zurich, Nikita Rudin
import torch
import torch.nn as nn
import torch.optim as optim
from rsl_rl.modules import ActorCritic
from rsl_rl.storage import RolloutStorage
class PPO:
actor_critic: ActorCritic
def __init__(self,
actor_critic,
num_learning_epochs=1,
num_mini_batches=1,
clip_param=0.2,
gamma=0.998,
lam=0.95,
value_loss_coef=1.0,
entropy_coef=0.0,
learning_rate=1e-3,
max_grad_norm=1.0,
use_clipped_value_loss=True,
schedule="fixed",
desired_kl=0.01,
device='cpu',
):
self.device = device
self.desired_kl = desired_kl
self.schedule = schedule
self.learning_rate = learning_rate
# PPO components
self.actor_critic = actor_critic
self.actor_critic.to(self.device)
self.storage = None # initialized later
self.optimizer = optim.Adam(self.actor_critic.parameters(), lr=learning_rate)
self.transition = RolloutStorage.Transition()
# PPO parameters
self.clip_param = clip_param
self.num_learning_epochs = num_learning_epochs
self.num_mini_batches = num_mini_batches
self.value_loss_coef = value_loss_coef
self.entropy_coef = entropy_coef
self.gamma = gamma
self.lam = lam
self.max_grad_norm = max_grad_norm
self.use_clipped_value_loss = use_clipped_value_loss
def init_storage(self, num_envs, num_transitions_per_env, actor_obs_shape, critic_obs_shape, action_shape):
self.storage = RolloutStorage(num_envs, num_transitions_per_env, actor_obs_shape, critic_obs_shape, action_shape, self.device)
def test_mode(self):
self.actor_critic.test()
def train_mode(self):
self.actor_critic.train()
def act(self, obs, critic_obs):
if self.actor_critic.is_recurrent:
self.transition.hidden_states = self.actor_critic.get_hidden_states()
# Compute the actions and values
self.transition.actions = self.actor_critic.act(obs).detach()
self.transition.values = self.actor_critic.evaluate(critic_obs).detach()
self.transition.actions_log_prob = self.actor_critic.get_actions_log_prob(self.transition.actions).detach()
self.transition.action_mean = self.actor_critic.action_mean.detach()
self.transition.action_sigma = self.actor_critic.action_std.detach()
# need to record obs and critic_obs before env.step()
self.transition.observations = obs
self.transition.critic_observations = critic_obs
return self.transition.actions
def process_env_step(self, rewards, dones, infos):
self.transition.rewards = rewards.clone()
self.transition.dones = dones
# Bootstrapping on time outs
if 'time_outs' in infos:
self.transition.rewards += self.gamma * torch.squeeze(self.transition.values * infos['time_outs'].unsqueeze(1).to(self.device), 1)
# Record the transition
self.storage.add_transitions(self.transition)
self.transition.clear()
self.actor_critic.reset(dones)
def compute_returns(self, last_critic_obs):
last_values= self.actor_critic.evaluate(last_critic_obs).detach()
self.storage.compute_returns(last_values, self.gamma, self.lam)
def update(self):
mean_value_loss = 0
mean_surrogate_loss = 0
if self.actor_critic.is_recurrent:
generator = self.storage.reccurent_mini_batch_generator(self.num_mini_batches, self.num_learning_epochs)
else:
generator = self.storage.mini_batch_generator(self.num_mini_batches, self.num_learning_epochs)
for obs_batch, critic_obs_batch, actions_batch, target_values_batch, advantages_batch, returns_batch, old_actions_log_prob_batch, \
old_mu_batch, old_sigma_batch, hid_states_batch, masks_batch in generator:
self.actor_critic.act(obs_batch, masks=masks_batch, hidden_states=hid_states_batch[0])
actions_log_prob_batch = self.actor_critic.get_actions_log_prob(actions_batch)
value_batch = self.actor_critic.evaluate(critic_obs_batch, masks=masks_batch, hidden_states=hid_states_batch[1])
mu_batch = self.actor_critic.action_mean
sigma_batch = self.actor_critic.action_std
entropy_batch = self.actor_critic.entropy
# KL
if self.desired_kl != None and self.schedule == 'adaptive':
with torch.inference_mode():
kl = torch.sum(
torch.log(sigma_batch / old_sigma_batch + 1.e-5) + (torch.square(old_sigma_batch) + torch.square(old_mu_batch - mu_batch)) / (2.0 * torch.square(sigma_batch)) - 0.5, axis=-1)
kl_mean = torch.mean(kl)
if kl_mean > self.desired_kl * 2.0:
self.learning_rate = max(1e-5, self.learning_rate / 1.5)
elif kl_mean < self.desired_kl / 2.0 and kl_mean > 0.0:
self.learning_rate = min(1e-2, self.learning_rate * 1.5)
for param_group in self.optimizer.param_groups:
param_group['lr'] = self.learning_rate
# Surrogate loss
ratio = torch.exp(actions_log_prob_batch - torch.squeeze(old_actions_log_prob_batch))
surrogate = -torch.squeeze(advantages_batch) * ratio
surrogate_clipped = -torch.squeeze(advantages_batch) * torch.clamp(ratio, 1.0 - self.clip_param,
1.0 + self.clip_param)
surrogate_loss = torch.max(surrogate, surrogate_clipped).mean()
# Value function loss
if self.use_clipped_value_loss:
value_clipped = target_values_batch + (value_batch - target_values_batch).clamp(-self.clip_param,
self.clip_param)
value_losses = (value_batch - returns_batch).pow(2)
value_losses_clipped = (value_clipped - returns_batch).pow(2)
value_loss = torch.max(value_losses, value_losses_clipped).mean()
else:
value_loss = (returns_batch - value_batch).pow(2).mean()
loss = surrogate_loss + self.value_loss_coef * value_loss - self.entropy_coef * entropy_batch.mean()
# Gradient step
self.optimizer.zero_grad()
loss.backward()
nn.utils.clip_grad_norm_(self.actor_critic.parameters(), self.max_grad_norm)
self.optimizer.step()
mean_value_loss += value_loss.item()
mean_surrogate_loss += surrogate_loss.item()
num_updates = self.num_learning_epochs * self.num_mini_batches
mean_value_loss /= num_updates
mean_surrogate_loss /= num_updates
self.storage.clear()
return mean_value_loss, mean_surrogate_loss

31
rsl_rl/rsl_rl/env/__init__.py vendored Normal file
View File

@@ -0,0 +1,31 @@
# SPDX-FileCopyrightText: Copyright (c) 2021 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: BSD-3-Clause
#
# 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.
#
# Copyright (c) 2021 ETH Zurich, Nikita Rudin
from .vec_env import VecEnv

60
rsl_rl/rsl_rl/env/vec_env.py vendored Normal file
View File

@@ -0,0 +1,60 @@
# SPDX-FileCopyrightText: Copyright (c) 2021 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: BSD-3-Clause
#
# 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.
#
# Copyright (c) 2021 ETH Zurich, Nikita Rudin
from abc import ABC, abstractmethod
import torch
from typing import Tuple, Union
# minimal interface of the environment
class VecEnv(ABC):
num_envs: int
num_obs: int
num_privileged_obs: int
num_actions: int
max_episode_length: int
privileged_obs_buf: torch.Tensor
obs_buf: torch.Tensor
rew_buf: torch.Tensor
reset_buf: torch.Tensor
episode_length_buf: torch.Tensor # current episode duration
extras: dict
device: torch.device
@abstractmethod
def step(self, actions: torch.Tensor) -> Tuple[torch.Tensor, Union[torch.Tensor, None], torch.Tensor, torch.Tensor, dict]:
pass
@abstractmethod
def reset(self, env_ids: Union[list, torch.Tensor]):
pass
@abstractmethod
def get_observations(self) -> torch.Tensor:
pass
@abstractmethod
def get_privileged_observations(self) -> Union[torch.Tensor, None]:
pass

View File

@@ -0,0 +1,34 @@
# SPDX-FileCopyrightText: Copyright (c) 2021 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: BSD-3-Clause
#
# 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.
#
# Copyright (c) 2021 ETH Zurich, Nikita Rudin
from .actor_critic import ActorCritic
from .actor_critic_recurrent import ActorCriticRecurrent
from .actor_critic_cts import ActorCriticCTS
from .actor_critic_moe_cts import ActorCriticMoECTS

View File

@@ -0,0 +1,155 @@
# SPDX-FileCopyrightText: Copyright (c) 2021 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: BSD-3-Clause
#
# 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.
#
# Copyright (c) 2021 ETH Zurich, Nikita Rudin
import numpy as np
import torch
import torch.nn as nn
from torch.distributions import Normal
from torch.nn.modules import rnn
class ActorCritic(nn.Module):
is_recurrent = False
def __init__(self, num_actor_obs,
num_critic_obs,
num_actions,
actor_hidden_dims=[256, 256, 256],
critic_hidden_dims=[256, 256, 256],
activation='elu',
init_noise_std=1.0,
**kwargs):
if kwargs:
print("ActorCritic.__init__ got unexpected arguments, which will be ignored: " + str([key for key in kwargs.keys()]))
super(ActorCritic, self).__init__()
activation = get_activation(activation)
mlp_input_dim_a = num_actor_obs
mlp_input_dim_c = num_critic_obs
# Policy
actor_layers = []
actor_layers.append(nn.Linear(mlp_input_dim_a, actor_hidden_dims[0]))
actor_layers.append(activation)
for l in range(len(actor_hidden_dims)):
if l == len(actor_hidden_dims) - 1:
actor_layers.append(nn.Linear(actor_hidden_dims[l], num_actions))
else:
actor_layers.append(nn.Linear(actor_hidden_dims[l], actor_hidden_dims[l + 1]))
actor_layers.append(activation)
self.actor = nn.Sequential(*actor_layers)
# Value function
critic_layers = []
critic_layers.append(nn.Linear(mlp_input_dim_c, critic_hidden_dims[0]))
critic_layers.append(activation)
for l in range(len(critic_hidden_dims)):
if l == len(critic_hidden_dims) - 1:
critic_layers.append(nn.Linear(critic_hidden_dims[l], 1))
else:
critic_layers.append(nn.Linear(critic_hidden_dims[l], critic_hidden_dims[l + 1]))
critic_layers.append(activation)
self.critic = nn.Sequential(*critic_layers)
print(f"Actor MLP: {self.actor}")
print(f"Critic MLP: {self.critic}")
# Action noise
self.std = nn.Parameter(init_noise_std * torch.ones(num_actions))
self.distribution = None
# disable args validation for speedup
Normal.set_default_validate_args = False
# seems that we get better performance without init
# self.init_memory_weights(self.memory_a, 0.001, 0.)
# self.init_memory_weights(self.memory_c, 0.001, 0.)
@staticmethod
# not used at the moment
def init_weights(sequential, scales):
[torch.nn.init.orthogonal_(module.weight, gain=scales[idx]) for idx, module in
enumerate(mod for mod in sequential if isinstance(mod, nn.Linear))]
def reset(self, dones=None):
pass
def forward(self):
raise NotImplementedError
@property
def action_mean(self):
return self.distribution.mean
@property
def action_std(self):
return self.distribution.stddev
@property
def entropy(self):
return self.distribution.entropy().sum(dim=-1)
def update_distribution(self, observations):
mean = self.actor(observations)
self.distribution = Normal(mean, mean*0. + self.std)
def act(self, observations, **kwargs):
self.update_distribution(observations)
return self.distribution.sample()
def get_actions_log_prob(self, actions):
return self.distribution.log_prob(actions).sum(dim=-1)
def act_inference(self, observations):
actions_mean = self.actor(observations)
return actions_mean
def evaluate(self, critic_observations, **kwargs):
value = self.critic(critic_observations)
return value
def get_activation(act_name):
if act_name == "elu":
return nn.ELU()
elif act_name == "selu":
return nn.SELU()
elif act_name == "relu":
return nn.ReLU()
elif act_name == "crelu":
return nn.ReLU()
elif act_name == "lrelu":
return nn.LeakyReLU()
elif act_name == "tanh":
return nn.Tanh()
elif act_name == "sigmoid":
return nn.Sigmoid()
else:
print("invalid activation function!")
return None

View File

@@ -0,0 +1,243 @@
# SPDX-FileCopyrightText: Copyright (c) 2021 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: BSD-3-Clause
#
# 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.
#
# Copyright (c) 2021 ETH Zurich, Nikita Rudin
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.distributions import Normal
class ActorCriticCTS(nn.Module):
is_recurrent = False
def __init__(self, num_actor_obs,
num_critic_obs,
num_actions,
num_envs,
history_length,
actor_hidden_dims=[512, 256, 128],
critic_hidden_dims=[512, 256, 128],
teacher_encoder_hidden_dims=[512, 256],
student_encoder_hidden_dims=[512, 256],
activation='elu',
init_noise_std=1.0,
latent_dim=32,
norm_type='l2norm',
**kwargs):
if kwargs:
print("ActorCritic.__init__ got unexpected arguments, which will be ignored: " + str([key for key in kwargs.keys()]))
assert norm_type in ['l2norm', 'simnorm'], f"Normalization type {norm_type} not supported!"
super(ActorCriticCTS, self).__init__()
self.num_actions = num_actions
activation = get_activation(activation)
mlp_input_dim_t = num_critic_obs
mlp_input_dim_s = num_actor_obs * history_length
mlp_input_dim_a = latent_dim + num_actor_obs
mlp_input_dim_c = latent_dim + num_critic_obs
# History
self.history = torch.zeros((num_envs, history_length, num_actor_obs), device='cuda')
# Teacher encoder
encoder_layers = []
encoder_layers.append(nn.Linear(mlp_input_dim_t, teacher_encoder_hidden_dims[0]))
encoder_layers.append(activation)
for l in range(len(teacher_encoder_hidden_dims)):
if l == len(teacher_encoder_hidden_dims) - 1:
encoder_layers.append(nn.Linear(teacher_encoder_hidden_dims[l], latent_dim))
if norm_type == 'l2norm':
encoder_layers.append(L2Norm())
elif norm_type == 'simnorm':
encoder_layers.append(SimNorm())
else:
encoder_layers.append(nn.Linear(teacher_encoder_hidden_dims[l], teacher_encoder_hidden_dims[l + 1]))
encoder_layers.append(activation)
self.teacher_encoder = nn.Sequential(*encoder_layers)
# Student encoder
encoder_layers = []
encoder_layers.append(nn.Linear(mlp_input_dim_s, student_encoder_hidden_dims[0]))
encoder_layers.append(activation)
for l in range(len(student_encoder_hidden_dims)):
if l == len(student_encoder_hidden_dims) - 1:
encoder_layers.append(nn.Linear(student_encoder_hidden_dims[l], latent_dim))
if norm_type == 'l2norm':
encoder_layers.append(L2Norm())
elif norm_type == 'simnorm':
encoder_layers.append(SimNorm())
else:
encoder_layers.append(nn.Linear(student_encoder_hidden_dims[l], student_encoder_hidden_dims[l + 1]))
encoder_layers.append(activation)
self.student_encoder = nn.Sequential(*encoder_layers)
# Policy
actor_layers = []
actor_layers.append(nn.Linear(mlp_input_dim_a, actor_hidden_dims[0]))
actor_layers.append(activation)
for l in range(len(actor_hidden_dims)):
if l == len(actor_hidden_dims) - 1:
actor_layers.append(nn.Linear(actor_hidden_dims[l], num_actions))
else:
actor_layers.append(nn.Linear(actor_hidden_dims[l], actor_hidden_dims[l + 1]))
actor_layers.append(activation)
self.actor = nn.Sequential(*actor_layers)
# Value function
critic_layers = []
critic_layers.append(nn.Linear(mlp_input_dim_c, critic_hidden_dims[0]))
critic_layers.append(activation)
for l in range(len(critic_hidden_dims)):
if l == len(critic_hidden_dims) - 1:
critic_layers.append(nn.Linear(critic_hidden_dims[l], 1))
else:
critic_layers.append(nn.Linear(critic_hidden_dims[l], critic_hidden_dims[l + 1]))
critic_layers.append(activation)
self.critic = nn.Sequential(*critic_layers)
print(f"Actor MLP: {self.actor}")
print(f"Critic MLP: {self.critic}")
print(f"Teacher Encoder MLP: {self.teacher_encoder}")
print(f"Student Encoder MLP: {self.student_encoder}")
# Action noise
self.std = nn.Parameter(init_noise_std * torch.ones(num_actions))
self.distribution = None
# disable args validation for speedup
Normal.set_default_validate_args = False
# seems that we get better performance without init
# self.init_memory_weights(self.memory_a, 0.001, 0.)
# self.init_memory_weights(self.memory_c, 0.001, 0.)
@staticmethod
# not used at the moment
def init_weights(sequential, scales):
[torch.nn.init.orthogonal_(module.weight, gain=scales[idx]) for idx, module in
enumerate(mod for mod in sequential if isinstance(mod, nn.Linear))]
def reset(self, dones=None):
self.history[dones > 0] = 0.0
def forward(self):
raise NotImplementedError
@property
def action_mean(self):
return self.distribution.mean
@property
def action_std(self):
return self.distribution.stddev
@property
def entropy(self):
return self.distribution.entropy().sum(dim=-1)
def update_distribution(self, latent_and_obs):
mean = self.actor(latent_and_obs)
self.distribution = Normal(mean, mean*0. + self.std)
def act(self, obs, privileged_obs, history, is_teacher, **kwargs):
if is_teacher:
latent = self.teacher_encoder(privileged_obs)
else:
latent = self.student_encoder(history).detach()
x = torch.cat([latent, obs], dim=1)
self.update_distribution(x)
return self.distribution.sample()
def get_actions_log_prob(self, actions):
return self.distribution.log_prob(actions).sum(dim=-1)
def act_inference(self, obs):
self.history = torch.cat([self.history[:, 1:], obs.unsqueeze(1)], dim=1)
latent = self.student_encoder(self.history.flatten(1))
x = torch.cat([latent, obs], dim=1)
actions_mean = self.actor(x)
return actions_mean
def evaluate(self, privileged_obs, history, is_teacher, **kwargs):
if is_teacher:
latent = self.teacher_encoder(privileged_obs)
else:
latent = self.student_encoder(history)
x = torch.cat([latent.detach(), privileged_obs], dim=1)
value = self.critic(x)
return value
def get_activation(act_name):
if act_name == "elu":
return nn.ELU()
elif act_name == "selu":
return nn.SELU()
elif act_name == "relu":
return nn.ReLU()
elif act_name == "crelu":
return nn.ReLU()
elif act_name == "lrelu":
return nn.LeakyReLU()
elif act_name == "tanh":
return nn.Tanh()
elif act_name == "sigmoid":
return nn.Sigmoid()
else:
print("invalid activation function!")
return None
class L2Norm(nn.Module):
def __init__(self):
super().__init__()
def forward(self, x):
return F.normalize(x, p=2.0, dim=-1)
class SimNorm(nn.Module):
"""
Simplicial normalization.
Adapted from https://arxiv.org/abs/2204.00616.
"""
def __init__(self):
super().__init__()
self.dim = 8 # for latent dim 512
def forward(self, x):
shp = x.shape
target_shape = list(shp[:-1]) + [-1, self.dim]
x = x.view(target_shape)
x = F.softmax(x, dim=-1)
return x.view(shp)
def __repr__(self):
return f"SimNorm(dim={self.dim})"

View File

@@ -0,0 +1,301 @@
# SPDX-FileCopyrightText: Copyright (c) 2021 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: BSD-3-Clause
#
# 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.
#
# Copyright (c) 2021 ETH Zurich, Nikita Rudin
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.distributions import Normal
class ActorCriticMoECTS(nn.Module):
is_recurrent = False
def __init__(self, num_obs,
num_critic_obs,
num_actions,
num_envs,
history_length,
obs_no_goal_mask,
actor_hidden_dims=[512, 256, 128],
critic_hidden_dims=[512, 256, 128],
teacher_encoder_hidden_dims=[512, 256],
student_encoder_hidden_dims=[512, 256],
student_expert_num=8,
activation='elu',
init_noise_std=1.0,
latent_dim=32,
norm_type='l2norm',
**kwargs):
if kwargs:
print("ActorCritic.__init__ got unexpected arguments, which will be ignored: " + str([key for key in kwargs.keys()]))
assert norm_type in ['l2norm', 'simnorm'], f"Normalization type {norm_type} not supported!"
super(ActorCriticMoECTS, self).__init__()
self.num_actions = num_actions
self.history_length = history_length
self.register_buffer("obs_no_goal_mask", torch.tensor(obs_no_goal_mask, dtype=torch.bool), persistent=False)
activation_str = activation
activation = get_activation(activation)
mlp_input_dim_t = num_critic_obs
mlp_input_dim_e = torch.sum(self.obs_no_goal_mask).item() * history_length # exclude command inputs for expert
mlp_input_dim_g = num_obs * history_length # all obs for gating
mlp_input_dim_a = latent_dim + num_obs
mlp_input_dim_c = latent_dim + num_critic_obs
# History
self.register_buffer("history", torch.zeros((num_envs, history_length, num_obs)), persistent=False)
# Teacher encoder
encoder_layers = []
encoder_layers.append(nn.Linear(mlp_input_dim_t, teacher_encoder_hidden_dims[0]))
encoder_layers.append(activation)
for l in range(len(teacher_encoder_hidden_dims)):
if l == len(teacher_encoder_hidden_dims) - 1:
encoder_layers.append(nn.Linear(teacher_encoder_hidden_dims[l], latent_dim))
if norm_type == 'l2norm':
encoder_layers.append(L2Norm())
elif norm_type == 'simnorm':
encoder_layers.append(SimNorm())
else:
encoder_layers.append(nn.Linear(teacher_encoder_hidden_dims[l], teacher_encoder_hidden_dims[l + 1]))
encoder_layers.append(activation)
self.teacher_encoder = nn.Sequential(*encoder_layers)
# Student MoE encoder
self.student_moe_encoder = StudentMoEEncoder(
expert_dim=mlp_input_dim_e,
gating_dim=mlp_input_dim_g,
hidden_dims=student_encoder_hidden_dims,
expert_num=student_expert_num,
latent_dim=latent_dim,
activation=activation_str
)
# Policy
actor_layers = []
actor_layers.append(nn.Linear(mlp_input_dim_a, actor_hidden_dims[0]))
actor_layers.append(activation)
for l in range(len(actor_hidden_dims)):
if l == len(actor_hidden_dims) - 1:
actor_layers.append(nn.Linear(actor_hidden_dims[l], num_actions))
else:
actor_layers.append(nn.Linear(actor_hidden_dims[l], actor_hidden_dims[l + 1]))
actor_layers.append(activation)
self.actor = nn.Sequential(*actor_layers)
# Value function
critic_layers = []
critic_layers.append(nn.Linear(mlp_input_dim_c, critic_hidden_dims[0]))
critic_layers.append(activation)
for l in range(len(critic_hidden_dims)):
if l == len(critic_hidden_dims) - 1:
critic_layers.append(nn.Linear(critic_hidden_dims[l], 1))
else:
critic_layers.append(nn.Linear(critic_hidden_dims[l], critic_hidden_dims[l + 1]))
critic_layers.append(activation)
self.critic = nn.Sequential(*critic_layers)
print(f"Actor MLP: {self.actor}")
print(f"Critic MLP: {self.critic}")
print(f"Teacher Encoder: {self.teacher_encoder}")
print(f"Student MoE Encoder: {self.student_moe_encoder}")
# Action noise
self.std = nn.Parameter(init_noise_std * torch.ones(num_actions))
self.distribution = None
# disable args validation for speedup
Normal.set_default_validate_args = False
# seems that we get better performance without init
# self.init_memory_weights(self.memory_a, 0.001, 0.)
# self.init_memory_weights(self.memory_c, 0.001, 0.)
@staticmethod
# not used at the moment
def init_weights(sequential, scales):
[torch.nn.init.orthogonal_(module.weight, gain=scales[idx]) for idx, module in
enumerate(mod for mod in sequential if isinstance(mod, nn.Linear))]
def reset(self, dones=None):
self.history[dones > 0] = 0.0
def forward(self):
raise NotImplementedError
@property
def action_mean(self):
return self.distribution.mean
@property
def action_std(self):
return self.distribution.stddev
@property
def entropy(self):
return self.distribution.entropy().sum(dim=-1)
def update_distribution(self, latent_and_obs):
mean = self.actor(latent_and_obs)
self.distribution = Normal(mean, mean*0. + self.std)
def act(self, obs, privileged_obs, history, is_teacher, **kwargs):
if is_teacher:
latent = self.teacher_encoder(privileged_obs)
else:
with torch.no_grad():
latent, _ = self.get_student_latent_and_weights(history)
x = torch.cat([latent, obs], dim=1)
self.update_distribution(x)
return self.distribution.sample()
def get_actions_log_prob(self, actions):
return self.distribution.log_prob(actions).sum(dim=-1)
def act_inference(self, obs):
self.history = torch.cat([self.history[:, 1:], obs.unsqueeze(1)], dim=1)
latent, _ = self.get_student_latent_and_weights(self.history.flatten(1))
x = torch.cat([latent, obs], dim=1)
actions_mean = self.actor(x)
return actions_mean
def evaluate(self, privileged_obs, history, is_teacher, **kwargs):
if is_teacher:
latent = self.teacher_encoder(privileged_obs)
else:
latent, _ = self.get_student_latent_and_weights(history)
x = torch.cat([latent.detach(), privileged_obs], dim=1)
value = self.critic(x)
return value
def get_student_latent_and_weights(self, history):
B = history.shape[0]
history_no_goal = history.reshape(B, self.history_length, -1)[:, :, self.obs_no_goal_mask].reshape(B, -1)
return self.student_moe_encoder(history, history_no_goal)
class StudentMoEEncoder(nn.Module):
def __init__(
self,
expert_dim,
gating_dim,
hidden_dims=[512, 256],
expert_num=8,
expert_hidden_dim=128,
latent_dim=32,
activation='elu',
norm_type='l2norm',
):
super().__init__()
self.expert_num = expert_num
self.latent_dim = latent_dim
self.norm_layer = L2Norm() if norm_type == 'l2norm' else SimNorm()
activation = get_activation(activation)
# Expert networks
experts_layers = []
last_dim = expert_dim
for l in hidden_dims:
experts_layers.append(nn.Linear(last_dim, l))
experts_layers.append(activation)
last_dim = l
self.experts_backbone = nn.Sequential(*experts_layers)
self.experts_hidden = nn.Sequential(
nn.Linear(last_dim, expert_num * expert_hidden_dim),
activation
)
self.experts_out = nn.Linear(expert_hidden_dim, latent_dim)
# Gating network
gating_layers = []
last_dim = gating_dim
for l in hidden_dims:
gating_layers.append(nn.Linear(last_dim, l))
gating_layers.append(activation)
last_dim = l
gating_layers.append(nn.Linear(last_dim, expert_num))
gating_layers.append(nn.Softmax(dim=-1))
self.gating_network = nn.Sequential(*gating_layers)
def forward(self, obs, obs_no_goal):
weights = self.gating_network(obs) # (batch, expert_num)
shared_features = self.experts_backbone(obs_no_goal)
expert_hidden = self.experts_hidden(shared_features)
expert_hidden = expert_hidden.view(-1, self.expert_num, expert_hidden.shape[-1] // self.expert_num)
expert_latent = self.experts_out(expert_hidden) # (batch, expert_num, latent_dim)
latent = torch.sum(weights.unsqueeze(-1) * expert_latent, dim=1) # (batch, latent_dim)
latent = self.norm_layer(latent)
return latent, weights
def get_activation(act_name):
if act_name == "elu":
return nn.ELU()
elif act_name == "selu":
return nn.SELU()
elif act_name == "relu":
return nn.ReLU()
elif act_name == "crelu":
return nn.ReLU()
elif act_name == "lrelu":
return nn.LeakyReLU()
elif act_name == "tanh":
return nn.Tanh()
elif act_name == "sigmoid":
return nn.Sigmoid()
else:
print("invalid activation function!")
return None
class L2Norm(nn.Module):
def __init__(self):
super().__init__()
def forward(self, x):
return F.normalize(x, p=2.0, dim=-1)
class SimNorm(nn.Module):
"""
Simplicial normalization.
Adapted from https://arxiv.org/abs/2204.00616.
"""
def __init__(self):
super().__init__()
self.dim = 8 # for latent dim 512
def forward(self, x):
shp = x.shape
x = x.view(*shp[:-1], -1, self.dim)
x = F.softmax(x, dim=-1)
return x.view(*shp)
def __repr__(self):
return f"SimNorm(dim={self.dim})"

View File

@@ -0,0 +1,116 @@
# SPDX-FileCopyrightText: Copyright (c) 2021 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: BSD-3-Clause
#
# 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.
#
# Copyright (c) 2021 ETH Zurich, Nikita Rudin
import numpy as np
import torch
import torch.nn as nn
from torch.distributions import Normal
from torch.nn.modules import rnn
from .actor_critic import ActorCritic, get_activation
from rsl_rl.utils import unpad_trajectories
class ActorCriticRecurrent(ActorCritic):
is_recurrent = True
def __init__(self, num_actor_obs,
num_critic_obs,
num_actions,
actor_hidden_dims=[256, 256, 256],
critic_hidden_dims=[256, 256, 256],
activation='elu',
rnn_type='lstm',
rnn_hidden_size=256,
rnn_num_layers=1,
init_noise_std=1.0,
**kwargs):
if kwargs:
print("ActorCriticRecurrent.__init__ got unexpected arguments, which will be ignored: " + str(kwargs.keys()),)
super().__init__(num_actor_obs=rnn_hidden_size,
num_critic_obs=rnn_hidden_size,
num_actions=num_actions,
actor_hidden_dims=actor_hidden_dims,
critic_hidden_dims=critic_hidden_dims,
activation=activation,
init_noise_std=init_noise_std)
activation = get_activation(activation)
self.memory_a = Memory(num_actor_obs, type=rnn_type, num_layers=rnn_num_layers, hidden_size=rnn_hidden_size)
self.memory_c = Memory(num_critic_obs, type=rnn_type, num_layers=rnn_num_layers, hidden_size=rnn_hidden_size)
print(f"Actor RNN: {self.memory_a}")
print(f"Critic RNN: {self.memory_c}")
def reset(self, dones=None):
self.memory_a.reset(dones)
self.memory_c.reset(dones)
def act(self, observations, masks=None, hidden_states=None):
input_a = self.memory_a(observations, masks, hidden_states)
return super().act(input_a.squeeze(0))
def act_inference(self, observations):
input_a = self.memory_a(observations)
return super().act_inference(input_a.squeeze(0))
def evaluate(self, critic_observations, masks=None, hidden_states=None):
input_c = self.memory_c(critic_observations, masks, hidden_states)
return super().evaluate(input_c.squeeze(0))
def get_hidden_states(self):
return self.memory_a.hidden_states, self.memory_c.hidden_states
class Memory(torch.nn.Module):
def __init__(self, input_size, type='lstm', num_layers=1, hidden_size=256):
super().__init__()
# RNN
rnn_cls = nn.GRU if type.lower() == 'gru' else nn.LSTM
self.rnn = rnn_cls(input_size=input_size, hidden_size=hidden_size, num_layers=num_layers)
self.hidden_states = None
def forward(self, input, masks=None, hidden_states=None):
batch_mode = masks is not None
if batch_mode:
# batch mode (policy update): need saved hidden states
if hidden_states is None:
raise ValueError("Hidden states not passed to memory module during policy update")
out, _ = self.rnn(input, hidden_states)
out = unpad_trajectories(out, masks)
else:
# inference mode (collection): use hidden states of last step
out, self.hidden_states = self.rnn(input.unsqueeze(0), self.hidden_states)
return out
def reset(self, dones=None):
# When the RNN is an LSTM, self.hidden_states_a is a list with hidden_state and cell_state
for hidden_state in self.hidden_states:
hidden_state[..., dones, :] = 0.0

View File

@@ -0,0 +1,32 @@
# SPDX-FileCopyrightText: Copyright (c) 2021 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: BSD-3-Clause
#
# 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.
#
# Copyright (c) 2021 ETH Zurich, Nikita Rudin
from .on_policy_runner import OnPolicyRunner
from .on_policy_runner_cts import OnPolicyRunnerCTS

View File

@@ -0,0 +1,233 @@
# SPDX-FileCopyrightText: Copyright (c) 2021 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: BSD-3-Clause
#
# 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.
#
# Copyright (c) 2021 ETH Zurich, Nikita Rudin
import time
import os
from collections import deque
import statistics
from torch.utils.tensorboard import SummaryWriter
import torch
from rsl_rl.algorithms import PPO
from rsl_rl.modules import ActorCritic, ActorCriticRecurrent
from rsl_rl.env import VecEnv
class OnPolicyRunner:
def __init__(self,
env: VecEnv,
train_cfg,
log_dir=None,
device='cpu'):
self.cfg=train_cfg["runner"]
self.alg_cfg = train_cfg["algorithm"]
self.policy_cfg = train_cfg["policy"]
self.device = device
self.env = env
if self.env.num_privileged_obs is not None:
num_critic_obs = self.env.num_privileged_obs
else:
num_critic_obs = self.env.num_obs
actor_critic_class = eval(self.cfg["policy_class_name"]) # ActorCritic
actor_critic: ActorCritic = actor_critic_class( self.env.num_obs,
num_critic_obs,
self.env.num_actions,
**self.policy_cfg).to(self.device)
alg_class = eval(self.cfg["algorithm_class_name"]) # PPO
self.alg: PPO = alg_class(actor_critic, device=self.device, **self.alg_cfg)
self.num_steps_per_env = self.cfg["num_steps_per_env"]
self.save_interval = self.cfg["save_interval"]
# init storage and model
self.alg.init_storage(self.env.num_envs, self.num_steps_per_env, [self.env.num_obs], [self.env.num_privileged_obs], [self.env.num_actions])
# Log
self.log_dir = log_dir
self.writer = None
self.tot_timesteps = 0
self.tot_time = 0
self.current_learning_iteration = 0
_, _ = self.env.reset()
def learn(self, num_learning_iterations, init_at_random_ep_len=False):
# initialize writer
if self.log_dir is not None and self.writer is None:
self.writer = SummaryWriter(log_dir=self.log_dir, flush_secs=10)
if init_at_random_ep_len:
self.env.episode_length_buf = torch.randint_like(self.env.episode_length_buf, high=int(self.env.max_episode_length))
obs = self.env.get_observations()
privileged_obs = self.env.get_privileged_observations()
critic_obs = privileged_obs if privileged_obs is not None else obs
obs, critic_obs = obs.to(self.device), critic_obs.to(self.device)
self.alg.actor_critic.train() # switch to train mode (for dropout for example)
ep_infos = []
rewbuffer = deque(maxlen=100)
lenbuffer = deque(maxlen=100)
cur_reward_sum = torch.zeros(self.env.num_envs, dtype=torch.float, device=self.device)
cur_episode_length = torch.zeros(self.env.num_envs, dtype=torch.float, device=self.device)
tot_iter = self.current_learning_iteration + num_learning_iterations
for it in range(self.current_learning_iteration, tot_iter):
start = time.time()
# Rollout
with torch.inference_mode():
for i in range(self.num_steps_per_env):
actions = self.alg.act(obs, critic_obs)
obs, privileged_obs, rewards, dones, infos = self.env.step(actions)
critic_obs = privileged_obs if privileged_obs is not None else obs
obs, critic_obs, rewards, dones = obs.to(self.device), critic_obs.to(self.device), rewards.to(self.device), dones.to(self.device)
self.alg.process_env_step(rewards, dones, infos)
if self.log_dir is not None:
# Book keeping
if 'episode' in infos:
ep_infos.append(infos['episode'])
cur_reward_sum += rewards
cur_episode_length += 1
new_ids = (dones > 0).nonzero(as_tuple=False)
rewbuffer.extend(cur_reward_sum[new_ids][:, 0].cpu().numpy().tolist())
lenbuffer.extend(cur_episode_length[new_ids][:, 0].cpu().numpy().tolist())
cur_reward_sum[new_ids] = 0
cur_episode_length[new_ids] = 0
stop = time.time()
collection_time = stop - start
# Learning step
start = stop
self.alg.compute_returns(critic_obs)
mean_value_loss, mean_surrogate_loss = self.alg.update()
stop = time.time()
learn_time = stop - start
if self.log_dir is not None:
self.log(locals())
if it % self.save_interval == 0:
self.save(os.path.join(self.log_dir, 'model_{}.pt'.format(it)))
ep_infos.clear()
self.current_learning_iteration += num_learning_iterations
self.save(os.path.join(self.log_dir, 'model_{}.pt'.format(self.current_learning_iteration)))
def log(self, locs, width=80, pad=35):
self.tot_timesteps += self.num_steps_per_env * self.env.num_envs
self.tot_time += locs['collection_time'] + locs['learn_time']
iteration_time = locs['collection_time'] + locs['learn_time']
ep_string = f''
if locs['ep_infos']:
for key in locs['ep_infos'][0]:
infotensor = torch.tensor([], device=self.device)
for ep_info in locs['ep_infos']:
# handle scalar and zero dimensional tensor infos
if not isinstance(ep_info[key], torch.Tensor):
ep_info[key] = torch.Tensor([ep_info[key]])
if len(ep_info[key].shape) == 0:
ep_info[key] = ep_info[key].unsqueeze(0)
infotensor = torch.cat((infotensor, ep_info[key].to(self.device)))
value = torch.mean(infotensor)
self.writer.add_scalar('Episode/' + key, value, locs['it'])
ep_string += f"""{f'Mean episode {key}:':>{pad}} {value:.4f}\n"""
mean_std = self.alg.actor_critic.std.mean()
fps = int(self.num_steps_per_env * self.env.num_envs / (locs['collection_time'] + locs['learn_time']))
self.writer.add_scalar('Loss/value_function', locs['mean_value_loss'], locs['it'])
self.writer.add_scalar('Loss/surrogate', locs['mean_surrogate_loss'], locs['it'])
self.writer.add_scalar('Loss/learning_rate', self.alg.learning_rate, locs['it'])
self.writer.add_scalar('Policy/mean_noise_std', mean_std.item(), locs['it'])
self.writer.add_scalar('Perf/total_fps', fps, locs['it'])
self.writer.add_scalar('Perf/collection time', locs['collection_time'], locs['it'])
self.writer.add_scalar('Perf/learning_time', locs['learn_time'], locs['it'])
if len(locs['rewbuffer']) > 0:
self.writer.add_scalar('Train/mean_reward', statistics.mean(locs['rewbuffer']), locs['it'])
self.writer.add_scalar('Train/mean_episode_length', statistics.mean(locs['lenbuffer']), locs['it'])
self.writer.add_scalar('Train/mean_reward/time', statistics.mean(locs['rewbuffer']), self.tot_time)
self.writer.add_scalar('Train/mean_episode_length/time', statistics.mean(locs['lenbuffer']), self.tot_time)
str = f" \033[1m Learning iteration {locs['it']}/{self.current_learning_iteration + locs['num_learning_iterations']} \033[0m "
if len(locs['rewbuffer']) > 0:
log_string = (f"""{'#' * width}\n"""
f"""{str.center(width, ' ')}\n\n"""
f"""{'Computation:':>{pad}} {fps:.0f} steps/s (collection: {locs[
'collection_time']:.3f}s, learning {locs['learn_time']:.3f}s)\n"""
f"""{'Value function loss:':>{pad}} {locs['mean_value_loss']:.4f}\n"""
f"""{'Surrogate loss:':>{pad}} {locs['mean_surrogate_loss']:.4f}\n"""
f"""{'Mean action noise std:':>{pad}} {mean_std.item():.2f}\n"""
f"""{'Mean reward:':>{pad}} {statistics.mean(locs['rewbuffer']):.2f}\n"""
f"""{'Mean episode length:':>{pad}} {statistics.mean(locs['lenbuffer']):.2f}\n""")
# f"""{'Mean reward/step:':>{pad}} {locs['mean_reward']:.2f}\n"""
# f"""{'Mean episode length/episode:':>{pad}} {locs['mean_trajectory_length']:.2f}\n""")
else:
log_string = (f"""{'#' * width}\n"""
f"""{str.center(width, ' ')}\n\n"""
f"""{'Computation:':>{pad}} {fps:.0f} steps/s (collection: {locs[
'collection_time']:.3f}s, learning {locs['learn_time']:.3f}s)\n"""
f"""{'Value function loss:':>{pad}} {locs['mean_value_loss']:.4f}\n"""
f"""{'Surrogate loss:':>{pad}} {locs['mean_surrogate_loss']:.4f}\n"""
f"""{'Mean action noise std:':>{pad}} {mean_std.item():.2f}\n""")
# f"""{'Mean reward/step:':>{pad}} {locs['mean_reward']:.2f}\n"""
# f"""{'Mean episode length/episode:':>{pad}} {locs['mean_trajectory_length']:.2f}\n""")
log_string += ep_string
log_string += (f"""{'-' * width}\n"""
f"""{'Total timesteps:':>{pad}} {self.tot_timesteps}\n"""
f"""{'Iteration time:':>{pad}} {iteration_time:.2f}s\n"""
f"""{'Total time:':>{pad}} {self.tot_time:.2f}s\n"""
f"""{'ETA:':>{pad}} {self.tot_time / (locs['it'] + 1) * (
locs['num_learning_iterations'] - locs['it']):.1f}s\n""")
print(log_string)
def save(self, path, infos=None):
torch.save({
'model_state_dict': self.alg.actor_critic.state_dict(),
'optimizer_state_dict': self.alg.optimizer.state_dict(),
'iter': self.current_learning_iteration,
'infos': infos,
}, path)
def load(self, path, load_optimizer=True):
loaded_dict = torch.load(path)
self.alg.actor_critic.load_state_dict(loaded_dict['model_state_dict'])
if load_optimizer:
self.alg.optimizer.load_state_dict(loaded_dict['optimizer_state_dict'])
self.current_learning_iteration = loaded_dict['iter']
return loaded_dict['infos']
def get_inference_policy(self, device=None):
self.alg.actor_critic.eval() # switch to evaluation mode (dropout for example)
if device is not None:
self.alg.actor_critic.to(device)
return self.alg.actor_critic.act_inference

View File

@@ -0,0 +1,286 @@
# SPDX-FileCopyrightText: Copyright (c) 2021 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: BSD-3-Clause
#
# 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.
#
# Copyright (c) 2021 ETH Zurich, Nikita Rudin
import time
import os
from collections import deque
import statistics
from torch.utils.tensorboard import SummaryWriter
import torch
from rsl_rl.algorithms import CTS, MoECTS
from rsl_rl.modules import ActorCriticCTS, ActorCriticMoECTS
from rsl_rl.env import VecEnv
import yaml
import numpy as np
from pathlib import Path
from legged_gym.utils.helpers import class_to_dict
from typing import Union
def numpy_representer(dumper, data):
return dumper.represent_float(float(data))
def numpy_int_representer(dumper, data):
return dumper.represent_int(int(data))
# Add the numpy representer to yaml
yaml.add_representer(np.float32, numpy_representer, Dumper=yaml.SafeDumper)
yaml.add_representer(np.float64, numpy_representer, Dumper=yaml.SafeDumper)
yaml.add_representer(np.int32, numpy_int_representer, Dumper=yaml.SafeDumper)
yaml.add_representer(np.int64, numpy_int_representer, Dumper=yaml.SafeDumper)
class OnPolicyRunnerCTS:
def __init__(self,
env: VecEnv,
train_cfg,
log_dir=None,
device='cpu'):
self.cfg=train_cfg["runner"]
self.alg_cfg = train_cfg["algorithm"]
self.policy_cfg = train_cfg["policy"]
self.device = device
self.env = env
if self.env.num_privileged_obs is not None:
num_critic_obs = self.env.num_privileged_obs
else:
num_critic_obs = self.env.num_obs
history_length = train_cfg["history_length"]
actor_critic_class = eval(self.cfg["policy_class_name"])
model: Union[ActorCriticCTS, ActorCriticMoECTS] = actor_critic_class(
self.env.num_obs,
num_critic_obs,
self.env.num_actions,
self.env.num_envs,
history_length,
**self.policy_cfg).to(self.device)
alg_class = eval(self.cfg["algorithm_class_name"])
self.alg: Union[CTS, MoECTS] = alg_class(model, self.env.num_envs, history_length, device=self.device, **self.alg_cfg)
self.num_steps_per_env = self.cfg["num_steps_per_env"]
self.save_interval = self.cfg["save_interval"]
# init storage and model
self.alg.init_storage(self.env.num_envs, self.num_steps_per_env, [self.env.num_obs], [self.env.num_privileged_obs], [self.env.num_actions])
# init history
self.history = torch.zeros((self.env.num_envs, history_length, self.env.num_obs), device=self.device)
# Log
self.log_dir = log_dir
self.writer = None
self.tot_timesteps = 0
self.tot_time = 0
self.current_learning_iteration = 0
_, _ = self.env.reset()
if self.log_dir is not None and self.env.cfg.env.test is False:
Path(self.log_dir).mkdir(parents=True, exist_ok=True)
all_cfg = {"train_cfg": train_cfg, "env_cfg": class_to_dict(self.env.cfg)}
yaml.safe_dump(all_cfg, open(os.path.join(self.log_dir, 'config.yaml'), 'w'))
def learn(self, num_learning_iterations, init_at_random_ep_len=False):
# initialize writer
if self.log_dir is not None and self.writer is None:
self.writer = SummaryWriter(log_dir=self.log_dir, flush_secs=10)
if init_at_random_ep_len:
self.env.episode_length_buf = torch.randint_like(self.env.episode_length_buf, high=int(self.env.max_episode_length))
obs = self.env.get_observations()
privileged_obs = self.env.get_privileged_observations()
assert privileged_obs is not None
obs, privileged_obs = obs.to(self.device), privileged_obs.to(self.device)
self.history = torch.cat([self.history[:, 1:], obs.unsqueeze(1)], dim=1)
self.alg.model.train() # switch to train mode (for dropout for example)
ep_infos = []
teacher_rewbuffer = deque(maxlen=100)
teacher_lenbuffer = deque(maxlen=100)
student_rewbuffer = deque(maxlen=100)
student_lenbuffer = deque(maxlen=100)
cur_reward_sum = torch.zeros(self.env.num_envs, dtype=torch.float, device=self.device)
cur_episode_length = torch.zeros(self.env.num_envs, dtype=torch.float, device=self.device)
self.start_learning_iteration = self.current_learning_iteration
tot_iter = self.current_learning_iteration + num_learning_iterations
for it in range(self.current_learning_iteration, tot_iter):
start = time.time()
# Rollout
with torch.inference_mode():
for i in range(self.num_steps_per_env):
actions = self.alg.act(obs, privileged_obs, self.history.flatten(1))
obs, privileged_obs, rewards, dones, infos = self.env.step(actions)
obs, privileged_obs, rewards, dones = obs.to(self.device), privileged_obs.to(self.device), rewards.to(self.device), dones.to(self.device)
self.history[dones > 0] = 0.0
self.history = torch.cat([self.history[:, 1:], obs.unsqueeze(1)], dim=1)
self.alg.process_env_step(rewards, dones, infos)
if self.log_dir is not None:
# Book keeping
if 'episode' in infos:
ep_infos.append(infos['episode'])
cur_reward_sum += rewards
cur_episode_length += 1
new_ids = (dones > 0).nonzero(as_tuple=False)
if new_ids.shape[0]:
ti = self.alg.teacher_env_idxs
teacher_ids = new_ids[torch.isin(new_ids, ti)]
student_ids = new_ids[~torch.isin(new_ids, ti)]
teacher_rewbuffer.extend(cur_reward_sum[teacher_ids].cpu().numpy().tolist())
teacher_lenbuffer.extend(cur_episode_length[teacher_ids].cpu().numpy().tolist())
student_rewbuffer.extend(cur_reward_sum[student_ids].cpu().numpy().tolist())
student_lenbuffer.extend(cur_episode_length[student_ids].cpu().numpy().tolist())
cur_reward_sum[new_ids] = 0
cur_episode_length[new_ids] = 0
stop = time.time()
collection_time = stop - start
# Learning step
start = stop
self.alg.compute_returns(privileged_obs, self.history.flatten(1))
if self.cfg["algorithm_class_name"] == "CTS":
mean_value_loss, mean_surrogate_loss, mean_entropy_loss, mean_latent_loss = self.alg.update()
elif self.cfg["algorithm_class_name"] == "MoECTS":
mean_value_loss, mean_surrogate_loss, mean_entropy_loss, mean_latent_loss, mean_load_balance_loss = self.alg.update()
stop = time.time()
learn_time = stop - start
self.current_learning_iteration += 1
if self.log_dir is not None:
self.log(locals())
if it % self.save_interval == 0:
self.save(os.path.join(self.log_dir, 'model_{}.pt'.format(it)))
ep_infos.clear()
self.save(os.path.join(self.log_dir, 'model_{}.pt'.format(self.current_learning_iteration)))
def log(self, locs, width=80, pad=35):
self.tot_timesteps += self.num_steps_per_env * self.env.num_envs
self.tot_time += locs['collection_time'] + locs['learn_time']
iteration_time = locs['collection_time'] + locs['learn_time']
ep_string = f''
if locs['ep_infos']:
for key in locs['ep_infos'][0]:
infotensor = torch.tensor([], device=self.device)
for ep_info in locs['ep_infos']:
# handle scalar and zero dimensional tensor infos
if not isinstance(ep_info[key], torch.Tensor):
ep_info[key] = torch.Tensor([ep_info[key]])
if len(ep_info[key].shape) == 0:
ep_info[key] = ep_info[key].unsqueeze(0)
infotensor = torch.cat((infotensor, ep_info[key].to(self.device)))
value = torch.mean(infotensor)
if 'terrain' in key:
self.writer.add_scalar('Terrain/' + key, value, locs['it'])
else:
self.writer.add_scalar('Episode/' + key, value, locs['it'])
ep_string += f"""{f'Mean episode {key}:':>{pad}} {value:.4f}\n"""
mean_std = self.alg.model.std.mean()
fps = int(self.num_steps_per_env * self.env.num_envs / (locs['collection_time'] + locs['learn_time']))
self.writer.add_scalar('Loss/value_function', locs['mean_value_loss'], locs['it'])
self.writer.add_scalar('Loss/surrogate', locs['mean_surrogate_loss'], locs['it'])
self.writer.add_scalar('Loss/entropy', locs['mean_entropy_loss'], locs['it'])
self.writer.add_scalar('Loss/latent', locs['mean_latent_loss'], locs['it'])
if 'mean_load_balance_loss' in locs:
self.writer.add_scalar('Loss/load_balance', locs['mean_load_balance_loss'], locs['it'])
self.writer.add_scalar('Loss/learning_rate', self.alg.learning_rate, locs['it'])
self.writer.add_scalar('Policy/mean_noise_std', mean_std.item(), locs['it'])
self.writer.add_scalar('Perf/total_fps', fps, locs['it'])
self.writer.add_scalar('Perf/collection time', locs['collection_time'], locs['it'])
self.writer.add_scalar('Perf/learning_time', locs['learn_time'], locs['it'])
if len(locs['teacher_rewbuffer']) > 0:
self.writer.add_scalar('Train/mean_teacher_reward', statistics.mean(locs['teacher_rewbuffer']), locs['it'])
self.writer.add_scalar('Train/mean_teacher_episode_length', statistics.mean(locs['teacher_lenbuffer']), locs['it'])
self.writer.add_scalar('Train/mean_teacher_reward/time', statistics.mean(locs['teacher_rewbuffer']), self.tot_time)
self.writer.add_scalar('Train/mean_teacher_episode_length/time', statistics.mean(locs['teacher_lenbuffer']), self.tot_time)
if len(locs['student_rewbuffer']) > 0:
self.writer.add_scalar('Train/mean_student_reward', statistics.mean(locs['student_rewbuffer']), locs['it'])
self.writer.add_scalar('Train/mean_student_episode_length', statistics.mean(locs['student_lenbuffer']), locs['it'])
self.writer.add_scalar('Train/mean_student_reward/time', statistics.mean(locs['student_rewbuffer']), self.tot_time)
self.writer.add_scalar('Train/mean_student_episode_length/time', statistics.mean(locs['student_lenbuffer']), self.tot_time)
str = f" \033[1m Learning iteration {self.current_learning_iteration}/{locs['tot_iter']} \033[0m "
log_string = (f"""{'#' * width}\n"""
f"""{str.center(width, ' ')}\n\n"""
f"""{'Computation:':>{pad}} {fps:.0f} steps/s (collection: {locs[
'collection_time']:.3f}s, learning {locs['learn_time']:.3f}s)\n"""
f"""{'Value function loss:':>{pad}} {locs['mean_value_loss']:.4f}\n"""
f"""{'Surrogate loss:':>{pad}} {locs['mean_surrogate_loss']:.4f}\n"""
f"""{'Entropy loss:':>{pad}} {locs['mean_entropy_loss']:.4f}\n"""
f"""{'Latent loss:':>{pad}} {locs['mean_latent_loss']:.4f}\n""")
if 'mean_load_balance_loss' in locs:
log_string += f"""{'Load balance loss:':>{pad}} {locs['mean_load_balance_loss']:.4f}\n"""
log_string += f"""{'Mean action noise std:':>{pad}} {mean_std.item():.2f}\n"""
if len(locs['teacher_rewbuffer']):
log_string += (f"""{'Mean teacher reward:':>{pad}} {statistics.mean(locs['teacher_rewbuffer']):.2f}\n"""
f"""{'Mean teacher episode length:':>{pad}} {statistics.mean(locs['teacher_lenbuffer']):.2f}\n""")
if len(locs['student_rewbuffer']):
log_string += (f"""{'Mean student reward:':>{pad}} {statistics.mean(locs['student_rewbuffer']):.2f}\n"""
f"""{'Mean student episode length:':>{pad}} {statistics.mean(locs['student_lenbuffer']):.2f}\n""")
log_string += ep_string
log_string += (f"""{'-' * width}\n"""
f"""{'Total timesteps:':>{pad}} {self.tot_timesteps}\n"""
f"""{'Iteration time:':>{pad}} {iteration_time:.2f}s\n"""
f"""{'Total time:':>{pad}} {self.tot_time:.2f}s\n"""
f"""{'ETA:':>{pad}} {self.tot_time / (self.current_learning_iteration - self.start_learning_iteration) * (
locs['tot_iter'] - locs['it']):.1f}s\n""")
print(log_string)
def save(self, path, infos=None):
torch.save({
'model_state_dict': self.alg.model.state_dict(),
'optimizer1_state_dict': self.alg.optimizer1.state_dict(),
'optimizer2_state_dict': self.alg.optimizer2.state_dict(),
'iter': self.current_learning_iteration,
'infos': infos,
}, path)
def load(self, path, load_optimizer=True):
loaded_dict = torch.load(path)
self.alg.model.load_state_dict(loaded_dict['model_state_dict'])
if load_optimizer:
self.alg.optimizer1.load_state_dict(loaded_dict['optimizer1_state_dict'])
self.alg.optimizer2.load_state_dict(loaded_dict['optimizer2_state_dict'])
self.current_learning_iteration = loaded_dict['iter']
return loaded_dict['infos']
def get_inference_policy(self, device=None):
self.alg.model.eval() # switch to evaluation mode (dropout for example)
if device is not None:
self.alg.model.to(device)
return self.alg.model.act_inference

View File

@@ -0,0 +1,5 @@
# Copyright 2021 ETH Zurich, NVIDIA CORPORATION
# SPDX-License-Identifier: BSD-3-Clause
from .rollout_storage import RolloutStorage
from .rollout_storage_cts import RolloutStorageCTS

View File

@@ -0,0 +1,235 @@
# SPDX-FileCopyrightText: Copyright (c) 2021 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: BSD-3-Clause
#
# 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.
#
# Copyright (c) 2021 ETH Zurich, Nikita Rudin
import torch
import numpy as np
from rsl_rl.utils import split_and_pad_trajectories
class RolloutStorage:
class Transition:
def __init__(self):
self.observations = None
self.critic_observations = None
self.actions = None
self.rewards = None
self.dones = None
self.values = None
self.actions_log_prob = None
self.action_mean = None
self.action_sigma = None
self.hidden_states = None
def clear(self):
self.__init__()
def __init__(self, num_envs, num_transitions_per_env, obs_shape, privileged_obs_shape, actions_shape, device='cpu'):
self.device = device
self.obs_shape = obs_shape
self.privileged_obs_shape = privileged_obs_shape
self.actions_shape = actions_shape
# Core
self.observations = torch.zeros(num_transitions_per_env, num_envs, *obs_shape, device=self.device)
if privileged_obs_shape[0] is not None:
self.privileged_observations = torch.zeros(num_transitions_per_env, num_envs, *privileged_obs_shape, device=self.device)
else:
self.privileged_observations = None
self.rewards = torch.zeros(num_transitions_per_env, num_envs, 1, device=self.device)
self.actions = torch.zeros(num_transitions_per_env, num_envs, *actions_shape, device=self.device)
self.dones = torch.zeros(num_transitions_per_env, num_envs, 1, device=self.device).byte()
# For PPO
self.actions_log_prob = torch.zeros(num_transitions_per_env, num_envs, 1, device=self.device)
self.values = torch.zeros(num_transitions_per_env, num_envs, 1, device=self.device)
self.returns = torch.zeros(num_transitions_per_env, num_envs, 1, device=self.device)
self.advantages = torch.zeros(num_transitions_per_env, num_envs, 1, device=self.device)
self.mu = torch.zeros(num_transitions_per_env, num_envs, *actions_shape, device=self.device)
self.sigma = torch.zeros(num_transitions_per_env, num_envs, *actions_shape, device=self.device)
self.num_transitions_per_env = num_transitions_per_env
self.num_envs = num_envs
# rnn
self.saved_hidden_states_a = None
self.saved_hidden_states_c = None
self.step = 0
def add_transitions(self, transition: Transition):
if self.step >= self.num_transitions_per_env:
raise AssertionError("Rollout buffer overflow")
self.observations[self.step].copy_(transition.observations)
if self.privileged_observations is not None: self.privileged_observations[self.step].copy_(transition.critic_observations)
self.actions[self.step].copy_(transition.actions)
self.rewards[self.step].copy_(transition.rewards.view(-1, 1))
self.dones[self.step].copy_(transition.dones.view(-1, 1))
self.values[self.step].copy_(transition.values)
self.actions_log_prob[self.step].copy_(transition.actions_log_prob.view(-1, 1))
self.mu[self.step].copy_(transition.action_mean)
self.sigma[self.step].copy_(transition.action_sigma)
self._save_hidden_states(transition.hidden_states)
self.step += 1
def _save_hidden_states(self, hidden_states):
if hidden_states is None or hidden_states==(None, None):
return
# make a tuple out of GRU hidden state sto match the LSTM format
hid_a = hidden_states[0] if isinstance(hidden_states[0], tuple) else (hidden_states[0],)
hid_c = hidden_states[1] if isinstance(hidden_states[1], tuple) else (hidden_states[1],)
# initialize if needed
if self.saved_hidden_states_a is None:
self.saved_hidden_states_a = [torch.zeros(self.observations.shape[0], *hid_a[i].shape, device=self.device) for i in range(len(hid_a))]
self.saved_hidden_states_c = [torch.zeros(self.observations.shape[0], *hid_c[i].shape, device=self.device) for i in range(len(hid_c))]
# copy the states
for i in range(len(hid_a)):
self.saved_hidden_states_a[i][self.step].copy_(hid_a[i])
self.saved_hidden_states_c[i][self.step].copy_(hid_c[i])
def clear(self):
self.step = 0
def compute_returns(self, last_values, gamma, lam):
advantage = 0
for step in reversed(range(self.num_transitions_per_env)):
if step == self.num_transitions_per_env - 1:
next_values = last_values
else:
next_values = self.values[step + 1]
next_is_not_terminal = 1.0 - self.dones[step].float()
delta = self.rewards[step] + next_is_not_terminal * gamma * next_values - self.values[step]
advantage = delta + next_is_not_terminal * gamma * lam * advantage
self.returns[step] = advantage + self.values[step]
# Compute and normalize the advantages
self.advantages = self.returns - self.values
self.advantages = (self.advantages - self.advantages.mean()) / (self.advantages.std() + 1e-8)
def get_statistics(self):
done = self.dones
done[-1] = 1
flat_dones = done.permute(1, 0, 2).reshape(-1, 1)
done_indices = torch.cat((flat_dones.new_tensor([-1], dtype=torch.int64), flat_dones.nonzero(as_tuple=False)[:, 0]))
trajectory_lengths = (done_indices[1:] - done_indices[:-1])
return trajectory_lengths.float().mean(), self.rewards.mean()
def mini_batch_generator(self, num_mini_batches, num_epochs=8):
batch_size = self.num_envs * self.num_transitions_per_env
mini_batch_size = batch_size // num_mini_batches
indices = torch.randperm(num_mini_batches*mini_batch_size, requires_grad=False, device=self.device)
observations = self.observations.flatten(0, 1)
if self.privileged_observations is not None:
critic_observations = self.privileged_observations.flatten(0, 1)
else:
critic_observations = observations
actions = self.actions.flatten(0, 1)
values = self.values.flatten(0, 1)
returns = self.returns.flatten(0, 1)
old_actions_log_prob = self.actions_log_prob.flatten(0, 1)
advantages = self.advantages.flatten(0, 1)
old_mu = self.mu.flatten(0, 1)
old_sigma = self.sigma.flatten(0, 1)
for epoch in range(num_epochs):
for i in range(num_mini_batches):
start = i*mini_batch_size
end = (i+1)*mini_batch_size
batch_idx = indices[start:end]
obs_batch = observations[batch_idx]
critic_observations_batch = critic_observations[batch_idx]
actions_batch = actions[batch_idx]
target_values_batch = values[batch_idx]
returns_batch = returns[batch_idx]
old_actions_log_prob_batch = old_actions_log_prob[batch_idx]
advantages_batch = advantages[batch_idx]
old_mu_batch = old_mu[batch_idx]
old_sigma_batch = old_sigma[batch_idx]
yield obs_batch, critic_observations_batch, actions_batch, target_values_batch, advantages_batch, returns_batch, \
old_actions_log_prob_batch, old_mu_batch, old_sigma_batch, (None, None), None
# for RNNs only
def reccurent_mini_batch_generator(self, num_mini_batches, num_epochs=8):
padded_obs_trajectories, trajectory_masks = split_and_pad_trajectories(self.observations, self.dones)
if self.privileged_observations is not None:
padded_critic_obs_trajectories, _ = split_and_pad_trajectories(self.privileged_observations, self.dones)
else:
padded_critic_obs_trajectories = padded_obs_trajectories
mini_batch_size = self.num_envs // num_mini_batches
for ep in range(num_epochs):
first_traj = 0
for i in range(num_mini_batches):
start = i*mini_batch_size
stop = (i+1)*mini_batch_size
dones = self.dones.squeeze(-1)
last_was_done = torch.zeros_like(dones, dtype=torch.bool)
last_was_done[1:] = dones[:-1]
last_was_done[0] = True
trajectories_batch_size = torch.sum(last_was_done[:, start:stop])
last_traj = first_traj + trajectories_batch_size
masks_batch = trajectory_masks[:, first_traj:last_traj]
obs_batch = padded_obs_trajectories[:, first_traj:last_traj]
critic_obs_batch = padded_critic_obs_trajectories[:, first_traj:last_traj]
actions_batch = self.actions[:, start:stop]
old_mu_batch = self.mu[:, start:stop]
old_sigma_batch = self.sigma[:, start:stop]
returns_batch = self.returns[:, start:stop]
advantages_batch = self.advantages[:, start:stop]
values_batch = self.values[:, start:stop]
old_actions_log_prob_batch = self.actions_log_prob[:, start:stop]
# reshape to [num_envs, time, num layers, hidden dim] (original shape: [time, num_layers, num_envs, hidden_dim])
# then take only time steps after dones (flattens num envs and time dimensions),
# take a batch of trajectories and finally reshape back to [num_layers, batch, hidden_dim]
last_was_done = last_was_done.permute(1, 0)
hid_a_batch = [ saved_hidden_states.permute(2, 0, 1, 3)[last_was_done][first_traj:last_traj].transpose(1, 0).contiguous()
for saved_hidden_states in self.saved_hidden_states_a ]
hid_c_batch = [ saved_hidden_states.permute(2, 0, 1, 3)[last_was_done][first_traj:last_traj].transpose(1, 0).contiguous()
for saved_hidden_states in self.saved_hidden_states_c ]
# remove the tuple for GRU
hid_a_batch = hid_a_batch[0] if len(hid_a_batch)==1 else hid_a_batch
hid_c_batch = hid_c_batch[0] if len(hid_c_batch)==1 else hid_a_batch
yield obs_batch, critic_obs_batch, actions_batch, values_batch, advantages_batch, returns_batch, \
old_actions_log_prob_batch, old_mu_batch, old_sigma_batch, (hid_a_batch, hid_c_batch), masks_batch
first_traj = last_traj

View File

@@ -0,0 +1,216 @@
# SPDX-FileCopyrightText: Copyright (c) 2021 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: BSD-3-Clause
#
# 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.
#
# Copyright (c) 2021 ETH Zurich, Nikita Rudin
import torch
import numpy as np
from rsl_rl.utils import split_and_pad_trajectories
class RolloutStorageCTS:
class Transition:
def __init__(self):
self.observations = None
self.critic_observations = None
self.actions = None
self.rewards = None
self.dones = None
self.values = None
self.actions_log_prob = None
self.action_mean = None
self.action_sigma = None
self.hidden_states = None
self.history = None
def clear(self):
self.__init__()
def __init__(self, num_envs, teacher_num_envs, history_length, num_transitions_per_env, obs_shape, privileged_obs_shape, actions_shape, device='cpu'):
self.device = device
self.obs_shape = obs_shape
self.privileged_obs_shape = privileged_obs_shape
self.actions_shape = actions_shape
self.teacher_num_envs = teacher_num_envs
self.student_num_envs = num_envs - teacher_num_envs
self.history_length = history_length
# Core
self.observations = torch.zeros(num_transitions_per_env, num_envs, *obs_shape, device=self.device)
if privileged_obs_shape[0] is not None:
self.privileged_observations = torch.zeros(num_transitions_per_env, num_envs, *privileged_obs_shape, device=self.device)
else:
self.privileged_observations = None
self.rewards = torch.zeros(num_transitions_per_env, num_envs, 1, device=self.device)
self.actions = torch.zeros(num_transitions_per_env, num_envs, *actions_shape, device=self.device)
self.dones = torch.zeros(num_transitions_per_env, num_envs, 1, device=self.device).byte()
self.history = torch.zeros(num_transitions_per_env, num_envs, self.history_length * obs_shape[0], device=self.device)
# For PPO
self.actions_log_prob = torch.zeros(num_transitions_per_env, num_envs, 1, device=self.device)
self.values = torch.zeros(num_transitions_per_env, num_envs, 1, device=self.device)
self.returns = torch.zeros(num_transitions_per_env, num_envs, 1, device=self.device)
self.advantages = torch.zeros(num_transitions_per_env, num_envs, 1, device=self.device)
self.mu = torch.zeros(num_transitions_per_env, num_envs, *actions_shape, device=self.device)
self.sigma = torch.zeros(num_transitions_per_env, num_envs, *actions_shape, device=self.device)
self.num_transitions_per_env = num_transitions_per_env
self.num_envs = num_envs
# rnn
self.saved_hidden_states_a = None
self.saved_hidden_states_c = None
self.step = 0
def add_transitions(self, transition: Transition):
if self.step >= self.num_transitions_per_env:
raise AssertionError("Rollout buffer overflow")
self.observations[self.step].copy_(transition.observations)
if self.privileged_observations is not None: self.privileged_observations[self.step].copy_(transition.critic_observations)
self.actions[self.step].copy_(transition.actions)
self.history[self.step].copy_(transition.history)
self.rewards[self.step].copy_(transition.rewards.view(-1, 1))
self.dones[self.step].copy_(transition.dones.view(-1, 1))
self.values[self.step].copy_(transition.values)
self.actions_log_prob[self.step].copy_(transition.actions_log_prob.view(-1, 1))
self.mu[self.step].copy_(transition.action_mean)
self.sigma[self.step].copy_(transition.action_sigma)
self._save_hidden_states(transition.hidden_states)
self.step += 1
def _save_hidden_states(self, hidden_states):
if hidden_states is None or hidden_states==(None, None):
return
# make a tuple out of GRU hidden state sto match the LSTM format
hid_a = hidden_states[0] if isinstance(hidden_states[0], tuple) else (hidden_states[0],)
hid_c = hidden_states[1] if isinstance(hidden_states[1], tuple) else (hidden_states[1],)
# initialize if needed
if self.saved_hidden_states_a is None:
self.saved_hidden_states_a = [torch.zeros(self.observations.shape[0], *hid_a[i].shape, device=self.device) for i in range(len(hid_a))]
self.saved_hidden_states_c = [torch.zeros(self.observations.shape[0], *hid_c[i].shape, device=self.device) for i in range(len(hid_c))]
# copy the states
for i in range(len(hid_a)):
self.saved_hidden_states_a[i][self.step].copy_(hid_a[i])
self.saved_hidden_states_c[i][self.step].copy_(hid_c[i])
def clear(self):
self.step = 0
def compute_returns(self, last_values, gamma, lam):
advantage = 0
for step in reversed(range(self.num_transitions_per_env)):
if step == self.num_transitions_per_env - 1:
next_values = last_values
else:
next_values = self.values[step + 1]
next_is_not_terminal = 1.0 - self.dones[step].float()
delta = self.rewards[step] + next_is_not_terminal * gamma * next_values - self.values[step]
advantage = delta + next_is_not_terminal * gamma * lam * advantage
self.returns[step] = advantage + self.values[step]
# Compute and normalize the advantages
self.advantages = self.returns - self.values
self.advantages = (self.advantages - self.advantages.mean()) / (self.advantages.std() + 1e-8)
def get_statistics(self):
done = self.dones
done[-1] = 1
flat_dones = done.permute(1, 0, 2).reshape(-1, 1)
done_indices = torch.cat((flat_dones.new_tensor([-1], dtype=torch.int64), flat_dones.nonzero(as_tuple=False)[:, 0]))
trajectory_lengths = (done_indices[1:] - done_indices[:-1])
return trajectory_lengths.float().mean(), self.rewards.mean()
def mini_batch_generator(self, num_mini_batches, num_epochs=8):
teacher_samples_num = self.teacher_num_envs * self.num_transitions_per_env
student_samples_num = self.student_num_envs * self.num_transitions_per_env
teacher_mini_batch_size = teacher_samples_num // num_mini_batches
student_mini_batch_size = student_samples_num // num_mini_batches
teacher_indices = torch.randperm(teacher_samples_num, requires_grad=False, device=self.device)
student_indices = teacher_samples_num + torch.randperm(student_samples_num, requires_grad=False, device=self.device)
# observations = self.observations.flatten(0, 1)
# if self.privileged_observations is not None:
# critic_observations = self.privileged_observations.flatten(0, 1)
# else:
# critic_observations = observations
# actions = self.actions.flatten(0, 1)
# history = self.history.flatten(0, 1)
# values = self.values.flatten(0, 1)
# returns = self.returns.flatten(0, 1)
# old_actions_log_prob = self.actions_log_prob.flatten(0, 1)
# advantages = self.advantages.flatten(0, 1)
# old_mu = self.mu.flatten(0, 1)
# old_sigma = self.sigma.flatten(0, 1)
obs_dims = list(range(2, self.observations.dim()))
observations = self.observations.permute(1, 0, *obs_dims).flatten(0, 1)
if self.privileged_observations is not None:
critic_dims = list(range(2, self.privileged_observations.dim()))
critic_observations = self.privileged_observations.permute(1, 0, *critic_dims).flatten(0, 1)
else:
critic_observations = observations
action_dims = list(range(2, self.actions.dim()))
actions = self.actions.permute(1, 0, *action_dims).flatten(0, 1)
old_mu = self.mu.permute(1, 0, *action_dims).flatten(0, 1)
old_sigma = self.sigma.permute(1, 0, *action_dims).flatten(0, 1)
hist_dims = list(range(2, self.history.dim()))
history = self.history.permute(1, 0, *hist_dims).flatten(0, 1)
values = self.values.permute(1, 0, 2).flatten(0, 1)
returns = self.returns.permute(1, 0, 2).flatten(0, 1)
old_actions_log_prob = self.actions_log_prob.permute(1, 0, 2).flatten(0, 1)
advantages = self.advantages.permute(1, 0, 2).flatten(0, 1)
def get_teacher_student_samples(data, slice):
(i1, i2), (j1, j2) = slice
return torch.cat([data[teacher_indices[i1:i2]], data[student_indices[j1:j2]]], 0).detach()
for _ in range(num_epochs):
for i in range(num_mini_batches):
slice = (
(i * teacher_mini_batch_size, (i+1) * teacher_mini_batch_size),
(i * student_mini_batch_size, (i+1) * student_mini_batch_size),
)
obs_batch = get_teacher_student_samples(observations, slice)
critic_observations_batch = get_teacher_student_samples(critic_observations, slice)
actions_batch = get_teacher_student_samples(actions, slice)
target_values_batch = get_teacher_student_samples(values, slice)
returns_batch = get_teacher_student_samples(returns, slice)
old_actions_log_prob_batch = get_teacher_student_samples(old_actions_log_prob, slice)
advantages_batch = get_teacher_student_samples(advantages, slice)
old_mu_batch = get_teacher_student_samples(old_mu, slice)
old_sigma_batch = get_teacher_student_samples(old_sigma, slice)
history_batch = get_teacher_student_samples(history, slice)
yield obs_batch, critic_observations_batch, actions_batch, history_batch, target_values_batch, advantages_batch, returns_batch, \
old_actions_log_prob_batch, old_mu_batch, old_sigma_batch, (None, None), None

View File

@@ -0,0 +1,31 @@
# SPDX-FileCopyrightText: Copyright (c) 2021 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: BSD-3-Clause
#
# 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.
#
# Copyright (c) 2021 ETH Zurich, Nikita Rudin
from .utils import split_and_pad_trajectories, unpad_trajectories

View File

@@ -0,0 +1,71 @@
# SPDX-FileCopyrightText: Copyright (c) 2021 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: BSD-3-Clause
#
# 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.
#
# Copyright (c) 2021 ETH Zurich, Nikita Rudin
import torch
def split_and_pad_trajectories(tensor, dones):
""" Splits trajectories at done indices. Then concatenates them and padds with zeros up to the length og the longest trajectory.
Returns masks corresponding to valid parts of the trajectories
Example:
Input: [ [a1, a2, a3, a4 | a5, a6],
[b1, b2 | b3, b4, b5 | b6]
]
Output:[ [a1, a2, a3, a4], | [ [True, True, True, True],
[a5, a6, 0, 0], | [True, True, False, False],
[b1, b2, 0, 0], | [True, True, False, False],
[b3, b4, b5, 0], | [True, True, True, False],
[b6, 0, 0, 0] | [True, False, False, False],
] | ]
Assumes that the inputy has the following dimension order: [time, number of envs, aditional dimensions]
"""
dones = dones.clone()
dones[-1] = 1
# Permute the buffers to have order (num_envs, num_transitions_per_env, ...), for correct reshaping
flat_dones = dones.transpose(1, 0).reshape(-1, 1)
# Get length of trajectory by counting the number of successive not done elements
done_indices = torch.cat((flat_dones.new_tensor([-1], dtype=torch.int64), flat_dones.nonzero()[:, 0]))
trajectory_lengths = done_indices[1:] - done_indices[:-1]
trajectory_lengths_list = trajectory_lengths.tolist()
# Extract the individual trajectories
trajectories = torch.split(tensor.transpose(1, 0).flatten(0, 1),trajectory_lengths_list)
padded_trajectories = torch.nn.utils.rnn.pad_sequence(trajectories)
trajectory_masks = trajectory_lengths > torch.arange(0, tensor.shape[0], device=tensor.device).unsqueeze(1)
return padded_trajectories, trajectory_masks
def unpad_trajectories(trajectories, masks):
""" Does the inverse operation of split_and_pad_trajectories()
"""
# Need to transpose before and after the masking to have proper reshaping
return trajectories.transpose(1, 0)[masks.transpose(1, 0)].view(-1, trajectories.shape[0], trajectories.shape[-1]).transpose(1, 0)

16
rsl_rl/setup.py Normal file
View File

@@ -0,0 +1,16 @@
from setuptools import setup, find_packages
setup(name='rsl_rl',
version='1.0.2',
author='Nikita Rudin',
author_email='rudinn@ethz.ch',
license="BSD-3-Clause",
packages=find_packages(),
description='Fast and simple RL algorithms implemented in pytorch',
python_requires='>=3.6',
install_requires=[
"torch>=1.4.0",
"torchvision>=0.5.0",
"numpy>=1.16.4"
],
)

21
setup.py Normal file
View File

@@ -0,0 +1,21 @@
from setuptools import find_packages
from distutils.core import setup
setup(name='unitree_rl_gym',
version='1.0.0',
author='Unitree Robotics',
license="BSD-3-Clause",
packages=find_packages(),
author_email='support@unitree.com',
description='Template RL environments for Unitree Robots',
install_requires=[
'isaacgym',
'rsl-rl',
'matplotlib',
'numpy==1.20',
'tensorboard',
'mujoco==3.2.3',
'pyyaml',
'onnx==1.17.0',
'pygame'
])