feat(training-server): release V0.7.3 整合强化学习服务器
web-platform-ci / TypeScript, lint, unit, build (push) Has been cancelled
web-platform-ci / Playwright E2E (push) Has been cancelled
web-platform-ci / TypeScript, lint, unit, build (pull_request) Has been cancelled
web-platform-ci / Playwright E2E (pull_request) Has been cancelled

This commit is contained in:
2026-09-01 17:51:42 +08:00
parent 8ba83a8d0a
commit cffac29a03
52 changed files with 761454 additions and 29 deletions
+3
View File
@@ -28,3 +28,6 @@ build/
# Runtime logs # Runtime logs
*.log *.log
MUJOCO_LOG.TXT MUJOCO_LOG.TXT
training_server/rl/logs/
training_server/rl/wandb/
training_server/rl/outputs/
+10
View File
@@ -2,6 +2,16 @@
本项目的重要变更记录在此文件中,版本标签沿用仓库现有的 `V主版本.次版本[.修订版本]` 格式。 本项目的重要变更记录在此文件中,版本标签沿用仓库现有的 `V主版本.次版本[.修订版本]` 格式。
## [0.7.3] - 2026-09-01
### 新增
- 将默认 `Unitree-Go2-Flat` 训练所需的 PPO 环境、奖励/观测配置、ONNX 导出逻辑和 Go2 模型资产内置到 `training_server/rl/`
### 变更
- 训练服务默认使用仓库内置训练器,不再要求单独克隆 `unitree_rl_mjlab`;仍可通过 `--trainer-root` 覆盖。
## [0.7.2] - 2026-09-01 ## [0.7.2] - 2026-09-01
### 变更 ### 变更
+2 -2
View File
@@ -15,7 +15,7 @@
- V3 地图创作层:认证资产库支持点击添加、拖到画布落位和首个资产自动创建场景,并可通过表单与视口操纵器继续编辑、事务式应用及导出地图 ZIP - V3 地图创作层:认证资产库支持点击添加、拖到画布落位和首个资产自动创建场景,并可通过表单与视口操纵器继续编辑、事务式应用及导出地图 ZIP
- 浏览器内 Python 控制器(Pyodide - 浏览器内 Python 控制器(Pyodide
- ONNX 强化学习策略推理(ONNX Runtime Web - ONNX 强化学习策略推理(ONNX Runtime Web
- 可选的本机 mjlab 训练桥接服务 - 内置 Go2 PPO 任务的本机 mjlab 训练桥接服务
- 响应式工作区、源码编辑、性能监控和中文诊断 - 响应式工作区、源码编辑、性能监控和中文诊断
## 快速开始 ## 快速开始
@@ -56,7 +56,7 @@ npm run lint:python
```text ```text
. .
├── web_platform/ React、TypeScript、Three.js 前端与测试夹具 ├── web_platform/ React、TypeScript、Three.js 前端与测试夹具
├── training_server/ 可选的本机强化学习训练桥接服务 ├── training_server/ 本机训练桥接服务及内置 Go2 PPO 训练代码/资产
├── controllers/ 可导入平台的 Python 控制器示例 ├── controllers/ 可导入平台的 Python 控制器示例
├── package.json 根目录统一开发入口 ├── package.json 根目录统一开发入口
└── LICENSE Apache License 2.0 └── LICENSE Apache License 2.0
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "mujoco-web-platform", "name": "mujoco-web-platform",
"version": "0.7.2", "version": "0.7.3",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "mujoco-web-platform", "name": "mujoco-web-platform",
"version": "0.7.2", "version": "0.7.3",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@monaco-editor/react": "^4.7.0", "@monaco-editor/react": "^4.7.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "mujoco-web-platform", "name": "mujoco-web-platform",
"version": "0.7.2", "version": "0.7.3",
"description": "基于 MuJoCo WebAssembly 的本地机器人仿真与控制平台", "description": "基于 MuJoCo WebAssembly 的本地机器人仿真与控制平台",
"private": true, "private": true,
"type": "module", "type": "module",
+1 -1
View File
@@ -1,7 +1,7 @@
[tool.ruff] [tool.ruff]
target-version = "py312" target-version = "py312"
line-length = 100 line-length = 100
extend-exclude = [".venv", "build"] extend-exclude = [".venv", "build", "training_server/rl"]
[tool.ruff.lint] [tool.ruff.lint]
select = ["E", "F", "I", "UP", "B", "SIM"] select = ["E", "F", "I", "UP", "B", "SIM"]
+18 -14
View File
@@ -1,35 +1,39 @@
# 本地强化学习训练服务 # 本地强化学习训练服务
该服务把 Web 平台发出的受限训练请求转换为本机 `unitree_rl_mjlab/scripts/train.py` 子进程,并提供状态轮询、停止任务和 `policy.onnx` 下载接口。服务只绑定 `127.0.0.1`,不执行前端传入的任意命令。 该服务把 Web 平台发出的受限训练请求转换为本机训练子进程,并提供状态轮询、停止任务和 `policy.onnx` 下载接口。服务只绑定 `127.0.0.1`,不执行前端传入的任意命令。
仓库已在 [`rl/`](rl/) 内置 `Unitree-Go2-Flat` 所需的 PPO 训练代码、Go2 模型资产和 ONNX 导出逻辑,不再要求另外克隆 `unitree_rl_mjlab``mjlab`、PyTorch 等大型运行依赖仍需安装在本机训练环境中。
## 准备训练环境
使用仓库已有的 `.venv` 项目虚拟环境:
```bash
source .venv/bin/activate
python -m pip install -r training_server/rl/requirements.txt
```
## 启动 ## 启动
必须使用已安装 `mjlab`、PyTorch 和训练工程依赖的 Python 解释器: 使用已安装训练依赖的 Python 解释器启动服务
```bash ```bash
npm run training-server -- \ npm run training-server -- \
--trainer-root /path/to/unitree_rl_mjlab \ --trainer-python "$PWD/.venv/bin/python"
--trainer-python /path/to/training-env/bin/python
```
如:
```bash
npm run training-server -- --trainer-root /home/cen/Embodied_Workspace/unitree_rl_mjlab --trainer-python /home/cen/miniconda3/envs/unitree_rl_mjlab/bin/python
``` ```
服务启动时会在终端显示一个随机访问令牌。将该令牌填入前端“访问令牌”字段后再连接。令牌只保存在当前浏览器标签页的 `sessionStorage` 中。自动化启动时可固定令牌: 服务启动时会在终端显示一个随机访问令牌。将该令牌填入前端“访问令牌”字段后再连接。令牌只保存在当前浏览器标签页的 `sessionStorage` 中。自动化启动时可固定令牌:
```bash ```bash
MUJOCO_TRAINING_TOKEN='至少十六个字符的随机令牌' npm run training-server -- \ MUJOCO_TRAINING_TOKEN='至少十六个字符的随机令牌' npm run training-server -- \
--trainer-root /path/to/unitree_rl_mjlab --trainer-python /path/to/training-env/bin/python
``` ```
也可用 `UNITREE_RL_MJLAB_ROOT` 指定工程目录。默认端口是 `8765`。如果前端不是从 `localhost``127.0.0.1` 提供,可显式添加来源: 默认训练工程是仓库内的 `training_server/rl`。如需使用包含其他已注册任务的外部训练工程,仍可通过 `--trainer-root /path/to/trainer` `UNITREE_RL_MJLAB_ROOT` 覆盖。默认端口是 `8765`。如果前端不是从 `localhost``127.0.0.1` 提供,可显式添加来源:
```bash ```bash
python training_server/server.py \ python training_server/server.py \
--trainer-root /path/to/unitree_rl_mjlab \ --trainer-python /path/to/training-env/bin/python \
--allow-origin http://192.168.1.10:5173 --allow-origin http://192.168.1.10:5173
``` ```
@@ -43,7 +47,7 @@ python training_server/server.py \
- `DELETE /api/training/jobs/{id}`:停止训练; - `DELETE /api/training/jobs/{id}`:停止训练;
- `GET /api/training/jobs/{id}/artifacts/policy.onnx`:下载本次生成的策略。 - `GET /api/training/jobs/{id}/artifacts/policy.onnx`:下载本次生成的策略。
任务保存在服务内存中,服务重启后历史任务状态会丢失,但 mjlab 日志和 checkpoint 仍保留在训练工程中。 任务保存在服务内存中,服务重启后历史任务状态会丢失;训练日志、checkpoint 和 ONNX 产物保留在 `training_server/rl/logs/rsl_rl/`,使用外部训练工程时则保留在对应工程中。
## 测试 ## 测试
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2024 The Isaac Lab Project Developers
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+28
View File
@@ -0,0 +1,28 @@
# 内置 Go2 强化学习训练器
这里包含 `training_server` 默认任务 `Unitree-Go2-Flat` 所需的训练代码与资产:
- RSL-RL/PPO 训练入口;
- Go2 速度跟踪环境、观测、奖励、终止条件和课程配置;
- Go2 MJCF 与网格资产;
- checkpoint 保存及 `policy.onnx` 导出 runner。
这是 [unitreerobotics/unitree_rl_mjlab](https://github.com/unitreerobotics/unitree_rl_mjlab) 的最小内置子集,来源提交为 `1425b15f73bd4095f0df53709d7c389c3eb9e790`,按上游 Apache License 2.0 许可证分发,见 [`LICENSE`](LICENSE)。为避免复制与默认训练任务无关的机器人、真机部署和运动模仿代码,只保留了 Go2 速度任务依赖。
## 环境
从仓库根目录使用已有的 `.venv` 项目虚拟环境:
```bash
source .venv/bin/activate
python -m pip install -r training_server/rl/requirements.txt
```
训练服务会直接从本目录启动 `scripts/train.py`,不需要安装本目录自身。训练产物写入本目录的 `logs/rsl_rl/`(已被仓库忽略)。
可以脱离服务验证任务是否成功注册:
```bash
cd training_server/rl
python scripts/train.py Unitree-Go2-Flat --help
```
+9
View File
@@ -0,0 +1,9 @@
# 与内置 unitree_rl_mjlab 子集验证一致的训练栈。
# mjlab 1.2.0 会使用 MuJoCo 3.5 的 mjENBL_MULTICCD API,不能放任
# pip 将 mujoco/warp-lang 升级到较新的不兼容版本。
mjlab==1.2.0
mujoco==3.5.0
mujoco-warp==3.5.0
warp-lang==1.15.0
# RSL-RL 5.0.1 仍向 wandb.Settings 传递 start_method0.29 已删除该字段。
wandb==0.28.2
+235
View File
@@ -0,0 +1,235 @@
"""Script to train RL agent with RSL-RL."""
import logging
import os
import sys
from dataclasses import asdict, dataclass, field
from datetime import datetime
from pathlib import Path
from typing import Literal, cast
# 训练器作为仓库内置子集直接从 scripts/ 启动,不要求额外执行 pip install -e。
TRAINER_ROOT = Path(__file__).resolve().parents[1]
if str(TRAINER_ROOT) not in sys.path:
sys.path.insert(0, str(TRAINER_ROOT))
import tyro
import warp as wp
# mjlab 1.2.0 的 GPU CUDA-graph 检查仍访问旧公开路径 wp.context;
# Warp 1.15 已把实现移到 warp._src.context,但保留了相同 runtime 契约。
if not hasattr(wp, "context"):
from warp._src import context as warp_context
wp.context = warp_context # type: ignore[attr-defined]
from mjlab.envs import ManagerBasedRlEnv, ManagerBasedRlEnvCfg
from mjlab.rl import MjlabOnPolicyRunner, RslRlBaseRunnerCfg, RslRlVecEnvWrapper
from mjlab.tasks.registry import list_tasks, load_env_cfg, load_rl_cfg, load_runner_cls
from mjlab.tasks.tracking.mdp import MotionCommandCfg
from mjlab.utils.gpu import select_gpus
from mjlab.utils.os import dump_yaml, get_checkpoint_path
from mjlab.utils.torch import configure_torch_backends
from mjlab.utils.wrappers import VideoRecorder
@dataclass(frozen=True)
class TrainConfig:
env: ManagerBasedRlEnvCfg
agent: RslRlBaseRunnerCfg
motion_file: str | None = None
video: bool = False
video_length: int = 200
video_interval: int = 2000
enable_nan_guard: bool = False
torchrunx_log_dir: str | None = None
gpu_ids: list[int] | Literal["all"] | None = field(default_factory=lambda: [0])
@staticmethod
def from_task(task_id: str) -> "TrainConfig":
env_cfg = load_env_cfg(task_id)
agent_cfg = load_rl_cfg(task_id)
return TrainConfig(env=env_cfg, agent=agent_cfg)
def run_train(task_id: str, cfg: TrainConfig, log_dir: Path) -> None:
cuda_visible = os.environ.get("CUDA_VISIBLE_DEVICES", "")
if cuda_visible == "":
device = "cpu"
seed = cfg.agent.seed
rank = 0
else:
local_rank = int(os.environ.get("LOCAL_RANK", "0"))
rank = int(os.environ.get("RANK", "0"))
# Set EGL device to match the CUDA device.
os.environ["MUJOCO_EGL_DEVICE_ID"] = str(local_rank)
device = f"cuda:{local_rank}"
# Set seed to have diversity in different processes.
seed = cfg.agent.seed + local_rank
configure_torch_backends()
cfg.agent.seed = seed
cfg.env.seed = seed
print(f"[INFO] Training with: device={device}, seed={seed}, rank={rank}")
# Check if this is a tracking task by checking for motion command.
is_tracking_task = "motion" in cfg.env.commands and isinstance(
cfg.env.commands["motion"], MotionCommandCfg
)
if is_tracking_task:
if not cfg.motion_file:
raise ValueError("For tracking tasks, --motion-file must be set ...")
motion_path = Path(cfg.motion_file).expanduser().resolve()
if not motion_path.exists():
raise FileNotFoundError(f"Motion file not found: {motion_path}")
motion_cmd = cfg.env.commands["motion"]
assert isinstance(motion_cmd, MotionCommandCfg)
motion_cmd.motion_file = str(motion_path)
print(f"[INFO] Using motion file: {motion_cmd.motion_file}")
# Check if motion_file is already set (e.g., via CLI --env.commands.motion.motion-file).
if motion_cmd.motion_file and Path(motion_cmd.motion_file).exists():
print(f"[INFO] Using local motion file: {motion_cmd.motion_file}")
# Enable NaN guard if requested.
if cfg.enable_nan_guard:
cfg.env.sim.nan_guard.enabled = True
print(f"[INFO] NaN guard enabled, output dir: {cfg.env.sim.nan_guard.output_dir}")
if rank == 0:
print(f"[INFO] Logging experiment in directory: {log_dir}")
env = ManagerBasedRlEnv(
cfg=cfg.env, device=device, render_mode="rgb_array" if cfg.video else None
)
log_root_path = log_dir.parent # Go up from specific run dir to experiment dir.
resume_path: Path | None = None
if cfg.agent.resume:
# Load checkpoint from local filesystem.
resume_path = get_checkpoint_path(
log_root_path, cfg.agent.load_run, cfg.agent.load_checkpoint
)
# Only record videos on rank 0 to avoid multiple workers writing to the same files.
if cfg.video and rank == 0:
env = VideoRecorder(
env,
video_folder=Path(log_dir) / "videos" / "train",
step_trigger=lambda step: step % cfg.video_interval == 0,
video_length=cfg.video_length,
disable_logger=True,
)
print("[INFO] Recording videos during training.")
env = RslRlVecEnvWrapper(env, clip_actions=cfg.agent.clip_actions)
agent_cfg = asdict(cfg.agent)
env_cfg = asdict(cfg.env)
runner_cls = load_runner_cls(task_id)
if runner_cls is None:
runner_cls = MjlabOnPolicyRunner
runner_kwargs = {}
runner = runner_cls(env, agent_cfg, str(log_dir), device, **runner_kwargs)
runner.add_git_repo_to_log(__file__)
if resume_path is not None:
print(f"[INFO]: Loading model checkpoint from: {resume_path}")
runner.load(str(resume_path))
# Only write config files from rank 0 to avoid race conditions.
if rank == 0:
dump_yaml(log_dir / "params" / "env.yaml", env_cfg)
dump_yaml(log_dir / "params" / "agent.yaml", agent_cfg)
runner.learn(
num_learning_iterations=cfg.agent.max_iterations, init_at_random_ep_len=True
)
env.close()
def launch_training(task_id: str, args: TrainConfig | None = None):
args = args or TrainConfig.from_task(task_id)
# Create log directory once before launching workers.
log_root_path = Path("logs") / "rsl_rl" / args.agent.experiment_name
log_root_path.resolve()
log_dir_name = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
if args.agent.run_name:
log_dir_name += f"_{args.agent.run_name}"
log_dir = log_root_path / log_dir_name
# Select GPUs based on CUDA_VISIBLE_DEVICES and user specification.
selected_gpus, num_gpus = select_gpus(args.gpu_ids)
# Set environment variables for all modes.
if selected_gpus is None:
os.environ["CUDA_VISIBLE_DEVICES"] = ""
else:
os.environ["CUDA_VISIBLE_DEVICES"] = ",".join(map(str, selected_gpus))
os.environ["MUJOCO_GL"] = "egl"
if num_gpus <= 1:
# CPU or single GPU: run directly without torchrunx.
run_train(task_id, args, log_dir)
else:
# Multi-GPU: use torchrunx.
import torchrunx
# torchrunx redirects stdout to logging.
logging.basicConfig(level=logging.INFO)
# Configure torchrunx logging directory.
# Priority: 1) existing env var, 2) user flag, 3) default to {log_dir}/torchrunx.
if "TORCHRUNX_LOG_DIR" not in os.environ:
if args.torchrunx_log_dir is not None:
# User specified a value via flag (could be "" to disable).
os.environ["TORCHRUNX_LOG_DIR"] = args.torchrunx_log_dir
else:
# Default: put logs in training directory.
os.environ["TORCHRUNX_LOG_DIR"] = str(log_dir / "torchrunx")
print(f"[INFO] Launching training with {num_gpus} GPUs", flush=True)
torchrunx.Launcher(
hostnames=["localhost"],
workers_per_host=num_gpus,
backend=None, # Let rsl_rl handle process group initialization.
copy_env_vars=torchrunx.DEFAULT_ENV_VARS_FOR_COPY + ("MUJOCO*",),
).run(run_train, task_id, args, log_dir)
def main():
# Parse first argument to choose the task.
# Import tasks to populate the registry.
import mjlab.tasks # noqa: F401
import src.tasks
all_tasks = list_tasks()
chosen_task, remaining_args = tyro.cli(
tyro.extras.literal_type_from_choices(all_tasks),
add_help=False,
return_unknown_args=True,
config=mjlab.TYRO_FLAGS,
)
args = tyro.cli(
TrainConfig,
args=remaining_args,
default=TrainConfig.from_task(chosen_task),
prog=sys.argv[0] + f" {chosen_task}",
config=mjlab.TYRO_FLAGS,
)
del remaining_args
launch_training(task_id=chosen_task, args=args)
if __name__ == "__main__":
main()
+4
View File
@@ -0,0 +1,4 @@
from pathlib import Path
SRC_PATH: Path = Path(__file__).parent
@@ -0,0 +1,3 @@
"""仓库内置训练器支持的机器人资产。"""
from .unitree_go2.go2_constants import get_go2_robot_cfg as get_go2_robot_cfg
@@ -0,0 +1 @@
"""Unitree Go2 quadruped."""
@@ -0,0 +1,148 @@
"""Unitree Go2 constants."""
from pathlib import Path
import mujoco
from src import SRC_PATH
from mjlab.actuator import BuiltinPositionActuatorCfg
from mjlab.entity import EntityArticulationInfoCfg, EntityCfg
from mjlab.utils.actuator import ElectricActuator, reflected_inertia
from mjlab.utils.os import update_assets
from mjlab.utils.spec_config import CollisionCfg
##
# MJCF and assets.
##
GO2_XML: Path = (
SRC_PATH / "assets" / "robots" / "unitree_go2" / "xmls" / "go2.xml"
)
assert GO2_XML.exists()
def get_assets(meshdir: str) -> dict[str, bytes]:
assets: dict[str, bytes] = {}
update_assets(assets, GO2_XML.parent / "assets", meshdir)
return assets
def get_spec() -> mujoco.MjSpec:
spec = mujoco.MjSpec.from_file(str(GO2_XML))
spec.assets = get_assets(spec.meshdir)
return spec
##
# Actuator config.
##
GO2_ACTUATOR_HIP = BuiltinPositionActuatorCfg(
target_names_expr=(
".*hip_.*",
),
stiffness=20.0,
damping=1.0,
effort_limit=23.5,
armature=0.01,
)
GO2_ACTUATOR_THIGH = BuiltinPositionActuatorCfg(
target_names_expr=(
".*thigh_.*",
),
stiffness=20.0,
damping=1.0,
effort_limit=23.5,
armature=0.01,
)
GO2_ACTUATOR_CALF = BuiltinPositionActuatorCfg(
target_names_expr=(
".*calf_.*",
),
stiffness=40.0,
damping=2.0,
effort_limit=45,
armature=0.02,
)
##
# Keyframes.
##
INIT_STATE = EntityCfg.InitialStateCfg(
pos=(0.0, 0.0, 0.32),
joint_pos={
".*thigh_joint": 0.9,
".*calf_joint": -1.8,
".*R_hip_joint": 0.1,
".*L_hip_joint": -0.1,
},
joint_vel={".*": 0.0},
)
##
# Collision config.
##
_foot_regex = "^[FR][LR]_foot_collision$"
# This disables all collisions except the feet.
# Furthermore, feet self collisions are disabled.
FEET_ONLY_COLLISION = CollisionCfg(
geom_names_expr=(_foot_regex,),
contype=0,
conaffinity=1,
condim=3,
priority=1,
friction=(0.6,),
solimp=(0.9, 0.95, 0.023),
)
# This enables all collisions, excluding self collisions.
# Foot collisions are given custom condim, friction and solimp.
FULL_COLLISION = CollisionCfg(
geom_names_expr=(".*_collision",),
condim={_foot_regex: 3, ".*_collision": 1},
priority={_foot_regex: 1},
friction={_foot_regex: (0.6,)},
solimp={_foot_regex: (0.9, 0.95, 0.023)},
contype=1,
conaffinity=0,
)
##
# Final config.
##
GO2_ARTICULATION = EntityArticulationInfoCfg(
actuators=(
GO2_ACTUATOR_HIP,
GO2_ACTUATOR_THIGH,
GO2_ACTUATOR_CALF,
),
soft_joint_pos_limit_factor=0.9,
)
def get_go2_robot_cfg() -> EntityCfg:
"""Get a fresh Go2 robot configuration instance.
Returns a new EntityCfg instance each time to avoid mutation issues when
the config is shared across multiple places.
"""
return EntityCfg(
init_state=INIT_STATE,
collisions=(FULL_COLLISION,),
spec_fn=get_spec,
articulation=GO2_ARTICULATION,
)
if __name__ == "__main__":
import mujoco.viewer as viewer
from mjlab.entity.entity import Entity
robot = Entity(get_go2_robot_cfg())
viewer.launch(robot.spec.compile())
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
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
@@ -0,0 +1,161 @@
<mujoco model="go2">
<compiler angle="radian" meshdir="assets" autolimits="true" />
<default>
<default class="go2">
<default class="visual">
<geom type="mesh" contype="0" conaffinity="0" density="0" group="2"/>
</default>
<default class="collision">
<geom priority="1" condim="6" group="3"/>
</default>
<site rgba="1 0 0 1" group="5"/>
</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_link" pos="0 0 0.445" childclass="go2">
<inertial pos="0.021112 0 -0.005366" quat="-0.000543471 0.713435 -0.00173769 0.700719" mass="6.921" diaginertia="0.107027 0.0980771 0.0244531" />
<freejoint name="floating_base_joint"/>
<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="base1_collision" size="0.1881 0.04675 0.057" type="box" class="collision" />
<geom name="base2_collision" size="0.05 0.045" pos="0.285 0 0.01" type="cylinder" class="collision" />
<geom name="base3_collision" size="0.047" pos="0.293 0 -0.06" type="sphere" class="collision" />
<site name="imu" pos="-0.02557 0 0.04232" group="5"/>
<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" axis="1 0 0" range="-1.0472 1.0472"/>
<geom mesh="hip_0" material="metal" class="visual" />
<geom mesh="hip_1" material="gray" class="visual" />
<geom name="FL_hip_collision" size="0.046 0.02" pos="0 0.08 0" quat="1 1 0 0" type="cylinder" class="collision" />
<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" axis="0 1 0" range="-1.5708 3.4907"/>
<geom mesh="thigh_0" material="metal" class="visual" />
<geom mesh="thigh_1" material="gray" class="visual" />
<geom name="FL_thigh_collision" size="0.1065 0.01225 0.017" pos="0 0 -0.1065" quat="0.707107 0 0.707107 0" type="box" class="collision" />
<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" axis="0 1 0" range="-2.7227 -0.83776"/>
<geom mesh="calf_0" material="gray" class="visual" />
<geom mesh="calf_1" material="black" class="visual" />
<geom name="FL_calf1_collision" size="0.012 0.06" pos="0.008 0 -0.06" quat="0.994493 0 -0.104807 0" type="cylinder" class="collision" />
<geom name="FL_calf2_collision" size="0.011 0.0325" pos="0.02 0 -0.148" quat="0.999688 0 0.0249974 0" type="cylinder" class="collision" />
<geom pos="0 0 -0.213" mesh="foot" class="visual" material="black" />
<geom name="FL_foot_collision" pos="0 0 -0.213" type="sphere" size="0.022" class="collision"/>
<site name="FL" pos="0 0 -0.213" type="sphere" size="0.022"/>
</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" axis="1 0 0" range="-1.0472 1.0472"/>
<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 name="FR_hip_collision" size="0.046 0.02" pos="0 -0.08 0" quat="0.707107 0.707107 0 0" type="cylinder" class="collision" />
<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" axis="0 1 0" range="-1.5708 3.4907"/>
<geom mesh="thigh_mirror_0" material="metal" class="visual" />
<geom mesh="thigh_mirror_1" material="gray" class="visual" />
<geom name="FR_thigh_collision" size="0.1065 0.01225 0.017" pos="0 0 -0.1065" quat="0.707107 0 0.707107 0" type="box" class="collision" />
<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" axis="0 1 0" range="-2.7227 -0.83776"/>
<geom mesh="calf_mirror_0" material="gray" class="visual" />
<geom mesh="calf_mirror_1" material="black" class="visual" />
<geom name="FR_calf1_collision" size="0.013 0.06" pos="0.01 0 -0.06" quat="0.995004 0 -0.0998334 0" type="cylinder" class="collision" />
<geom name="FR_calf2_collision" size="0.011 0.0325" pos="0.02 0 -0.148" quat="0.999688 0 0.0249974 0" type="cylinder" class="collision" />
<geom pos="0 0 -0.213" mesh="foot" class="visual" material="black" />
<geom name="FR_foot_collision" pos="0 0 -0.213" type="sphere" size="0.022" class="collision"/>
<site name="FR" pos="0 0 -0.213" type="sphere" size="0.022"/>
</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" axis="1 0 0" range="-1.0472 1.0472"/>
<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 name="RL_hip_collision" size="0.046 0.02" pos="0 0.08 0" quat="0.707107 0.707107 0 0" type="cylinder" class="collision" />
<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" axis="0 1 0" range="-0.5236 4.5379"/>
<geom mesh="thigh_0" material="metal" class="visual" />
<geom mesh="thigh_1" material="gray" class="visual" />
<geom name="RL_thigh_collision" size="0.1065 0.01225 0.017" pos="0 0 -0.1065" quat="0.707107 0 0.707107 0" type="box" class="collision" />
<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" axis="0 1 0" range="-2.7227 -0.83776"/>
<geom mesh="calf_0" material="gray" class="visual" />
<geom mesh="calf_1" material="black" class="visual" />
<geom name="RL_calf1_collision" size="0.013 0.06" pos="0.01 0 -0.06" quat="0.995004 0 -0.0998334 0" type="cylinder" class="collision" />
<geom name="RL_calf2_collision" size="0.011 0.0325" pos="0.02 0 -0.148" quat="0.999688 0 0.0249974 0" type="cylinder" class="collision" />
<geom pos="0 0 -0.213" mesh="foot" class="visual" material="black" />
<geom name="RL_foot_collision" pos="0 0 -0.213" type="sphere" size="0.022" class="collision"/>
<site name="RL" pos="0 0 -0.213" type="sphere" size="0.022"/>
</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" axis="1 0 0" range="-1.0472 1.0472"/>
<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 name="RR_hip_collision" size="0.046 0.02" pos="0 -0.08 0" quat="0.707107 0.707107 0 0" type="cylinder" class="collision" />
<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" axis="0 1 0" range="-0.5236 4.5379"/>
<geom mesh="thigh_mirror_0" material="metal" class="visual" />
<geom mesh="thigh_mirror_1" material="gray" class="visual" />
<geom name="RR_thigh_collision" size="0.1065 0.01225 0.017" pos="0 0 -0.1065" quat="0.707107 0 0.707107 0" type="box" class="collision" />
<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" axis="0 1 0" range="-2.7227 -0.83776"/>
<geom mesh="calf_mirror_0" material="gray" class="visual" />
<geom mesh="calf_mirror_1" material="black" class="visual" />
<geom name="RR_calf1_collision" size="0.013 0.06" pos="0.01 0 -0.06" quat="0.995004 0 -0.0998334 0" type="cylinder" class="collision" />
<geom name="RR_calf2_collision" size="0.011 0.0325" pos="0.02 0 -0.148" quat="0.999688 0 0.0249974 0" type="cylinder" class="collision" />
<geom pos="0 0 -0.213" mesh="foot" class="visual" material="black" />
<geom name="RR_foot_collision" pos="0 0 -0.213" type="sphere" size="0.022" class="collision"/>
<site name="RR" pos="0 0 -0.213" type="sphere" size="0.022"/>
</body>
</body>
</body>
</body>
</worldbody>
<sensor>
<gyro name="imu_ang_vel" site="imu"/>
<velocimeter name="imu_lin_vel" site="imu"/>
<accelerometer name="imu_lin_acc" site="imu"/>
<subtreeangmom name="root_angmom" body="base_link"/>
</sensor>
</mujoco>
@@ -0,0 +1,306 @@
<mujoco model="scene_go2">
<compiler angle="radian" meshdir="assets" autolimits="true" />
<option cone="elliptic" impratio="100" />
<default>
<default class="go2">
<geom friction="0.4" margin="0.001" condim="1"/>
<joint axis="0 1 0" damping="0.1" armature="0.01" frictionloss="0.2"/>
<motor ctrlrange="-23.7 23.7"/>
<default class="abduction">
<joint axis="1 0 0" range="-1.0472 1.0472"/>
</default>
<default class="hip">
<default class="front_hip">
<joint range="-1.5708 3.4907"/>
</default>
<default class="back_hip">
<joint range="-0.5236 4.5379"/>
</default>
</default>
<default class="knee">
<joint range="-2.7227 -0.83776"/>
<motor ctrlrange="-45.43 45.43"/>
</default>
<default class="visual">
<geom type="mesh" contype="0" conaffinity="0" group="2"/>
</default>
<default class="collision">
<geom group="3"/>
<default class="foot">
<geom size="0.022" pos="-0.002 0 -0.213" priority="1" condim="6"
friction="0.4 0.02 0.01"/>
</default>
</default>
</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_link" pos="0 0 0.445" childclass="go2">
<inertial pos="0.021112 0 -0.005366" quat="-0.000543471 0.713435 -0.00173769 0.700719"
mass="6.921"
diaginertia="0.107027 0.0980771 0.0244531" />
<freejoint />
<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 size="0.1881 0.04675 0.057" type="box" class="collision" />
<geom size="0.05 0.045" pos="0.285 0 0.01" type="cylinder" class="collision" />
<geom size="0.047" pos="0.293 0 -0.06" class="collision" />
<site name="imu" pos="-0.02557 0 0.04232" />
<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" class="abduction" />
<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="1 1 0 0" type="cylinder" class="collision" />
<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" class="front_hip" />
<geom mesh="thigh_0" material="metal" class="visual" />
<geom mesh="thigh_1" material="gray" class="visual" />
<geom size="0.1065 0.01225 0.017" pos="0 0 -0.1065" quat="0.707107 0 0.707107 0"
type="box" class="collision" />
<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" class="knee" />
<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" class="collision" />
<geom size="0.011 0.0325" pos="0.02 0 -0.148" quat="0.999688 0 0.0249974 0"
type="cylinder" class="collision" />
<geom pos="0 0 -0.213" mesh="foot" class="visual" material="black" />
<geom name="FL" class="foot" />
<body name="FL_foot" pos="0 0 -0.213" />
</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" class="abduction" />
<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"
class="collision" />
<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" class="front_hip" />
<geom mesh="thigh_mirror_0" material="metal" class="visual" />
<geom mesh="thigh_mirror_1" material="gray" class="visual" />
<geom size="0.1065 0.01225 0.017" pos="0 0 -0.1065" quat="0.707107 0 0.707107 0"
type="box" class="collision" />
<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" class="knee" />
<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" class="collision" />
<geom size="0.011 0.0325" pos="0.02 0 -0.148" quat="0.999688 0 0.0249974 0"
type="cylinder" class="collision" />
<geom pos="0 0 -0.213" mesh="foot" class="visual" material="black" />
<geom name="FR" class="foot" />
<body name="FR_foot" pos="0 0 -0.213" />
</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" class="abduction" />
<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"
class="collision" />
<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" class="back_hip" />
<geom mesh="thigh_0" material="metal" class="visual" />
<geom mesh="thigh_1" material="gray" class="visual" />
<geom size="0.1065 0.01225 0.017" pos="0 0 -0.1065" quat="0.707107 0 0.707107 0"
type="box" class="collision" />
<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" class="knee" />
<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" class="collision" />
<geom size="0.011 0.0325" pos="0.02 0 -0.148" quat="0.999688 0 0.0249974 0"
type="cylinder" class="collision" />
<geom pos="0 0 -0.213" mesh="foot" class="visual" material="black" />
<geom name="RL" class="foot" />
<body name="RL_foot" pos="0 0 -0.213" />
</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" class="abduction" />
<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"
class="collision" />
<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" class="back_hip" />
<geom mesh="thigh_mirror_0" material="metal" class="visual" />
<geom mesh="thigh_mirror_1" material="gray" class="visual" />
<geom size="0.1065 0.01225 0.017" pos="0 0 -0.1065" quat="0.707107 0 0.707107 0"
type="box" class="collision" />
<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" class="knee" />
<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" class="collision" />
<geom size="0.011 0.0325" pos="0.02 0 -0.148" quat="0.999688 0 0.0249974 0"
type="cylinder" class="collision" />
<geom pos="0 0 -0.213" mesh="foot" class="visual" material="black" />
<geom name="RR" class="foot" />
<body name="RR_foot" pos="0 0 -0.213" />
</body>
</body>
</body>
</body>
</worldbody>
<actuator>
<motor class="abduction" name="FR_hip" joint="FR_hip_joint" />
<motor class="hip" name="FR_thigh" joint="FR_thigh_joint" />
<motor class="knee" name="FR_calf" joint="FR_calf_joint" />
<motor class="abduction" name="FL_hip" joint="FL_hip_joint" />
<motor class="hip" name="FL_thigh" joint="FL_thigh_joint" />
<motor class="knee" name="FL_calf" joint="FL_calf_joint" />
<motor class="abduction" name="RR_hip" joint="RR_hip_joint" />
<motor class="hip" name="RR_thigh" joint="RR_thigh_joint" />
<motor class="knee" name="RR_calf" joint="RR_calf_joint" />
<motor class="abduction" name="RL_hip" joint="RL_hip_joint" />
<motor class="hip" name="RL_thigh" joint="RL_thigh_joint" />
<motor class="knee" name="RL_calf" joint="RL_calf_joint" />
</actuator>
<sensor>
<jointpos name="FR_hip_pos" joint="FR_hip_joint" />
<jointpos name="FR_thigh_pos" joint="FR_thigh_joint" />
<jointpos name="FR_calf_pos" joint="FR_calf_joint" />
<jointpos name="FL_hip_pos" joint="FL_hip_joint" />
<jointpos name="FL_thigh_pos" joint="FL_thigh_joint" />
<jointpos name="FL_calf_pos" joint="FL_calf_joint" />
<jointpos name="RR_hip_pos" joint="RR_hip_joint" />
<jointpos name="RR_thigh_pos" joint="RR_thigh_joint" />
<jointpos name="RR_calf_pos" joint="RR_calf_joint" />
<jointpos name="RL_hip_pos" joint="RL_hip_joint" />
<jointpos name="RL_thigh_pos" joint="RL_thigh_joint" />
<jointpos name="RL_calf_pos" joint="RL_calf_joint" />
<jointvel name="FR_hip_vel" joint="FR_hip_joint" />
<jointvel name="FR_thigh_vel" joint="FR_thigh_joint" />
<jointvel name="FR_calf_vel" joint="FR_calf_joint" />
<jointvel name="FL_hip_vel" joint="FL_hip_joint" />
<jointvel name="FL_thigh_vel" joint="FL_thigh_joint" />
<jointvel name="FL_calf_vel" joint="FL_calf_joint" />
<jointvel name="RR_hip_vel" joint="RR_hip_joint" />
<jointvel name="RR_thigh_vel" joint="RR_thigh_joint" />
<jointvel name="RR_calf_vel" joint="RR_calf_joint" />
<jointvel name="RL_hip_vel" joint="RL_hip_joint" />
<jointvel name="RL_thigh_vel" joint="RL_thigh_joint" />
<jointvel name="RL_calf_vel" joint="RL_calf_joint" />
<jointactuatorfrc name="FR_hip_torque" joint="FR_hip_joint" noise="0.01" />
<jointactuatorfrc name="FR_thigh_torque" joint="FR_thigh_joint" noise="0.01" />
<jointactuatorfrc name="FR_calf_torque" joint="FR_calf_joint" noise="0.01" />
<jointactuatorfrc name="FL_hip_torque" joint="FL_hip_joint" noise="0.01" />
<jointactuatorfrc name="FL_thigh_torque" joint="FL_thigh_joint" noise="0.01" />
<jointactuatorfrc name="FL_calf_torque" joint="FL_calf_joint" noise="0.01" />
<jointactuatorfrc name="RR_hip_torque" joint="RR_hip_joint" noise="0.01" />
<jointactuatorfrc name="RR_thigh_torque" joint="RR_thigh_joint" noise="0.01" />
<jointactuatorfrc name="RR_calf_torque" joint="RR_calf_joint" noise="0.01" />
<jointactuatorfrc name="RL_hip_torque" joint="RL_hip_joint" noise="0.01" />
<jointactuatorfrc name="RL_thigh_torque" joint="RL_thigh_joint" noise="0.01" />
<jointactuatorfrc name="RL_calf_torque" joint="RL_calf_joint" noise="0.01" />
<framequat name="imu_quat" objtype="site" objname="imu" />
<gyro name="imu_gyro" site="imu" />
<accelerometer name="imu_acc" site="imu" />
<framepos name="frame_pos" objtype="site" objname="imu" />
<framelinvel name="frame_vel" objtype="site" objname="imu" />
</sensor>
<keyframe>
<key name="home" qpos="0 0 0.27 1 0 0 0 0 0.9 -1.8 0 0.9 -1.8 0 0.9 -1.8 0 0.9 -1.8"
ctrl="0 0.9 -1.8 0 0.9 -1.8 0 0.9 -1.8 0 0.9 -1.8" />
</keyframe>
<!-- setup scene -->
<statistic center="1.0 1.0 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>
+5
View File
@@ -0,0 +1,5 @@
from mjlab.utils.lab_api.tasks.importer import import_packages
_BLACKLIST_PKGS = ["utils", ".mdp"]
import_packages(__name__, _BLACKLIST_PKGS)
@@ -0,0 +1 @@
"""Velocity tracking environments for legged robots."""
@@ -0,0 +1,24 @@
from mjlab.tasks.registry import register_mjlab_task
from src.tasks.velocity.rl import VelocityOnPolicyRunner
from .env_cfgs import (
unitree_go2_flat_env_cfg,
unitree_go2_rough_env_cfg,
)
from .rl_cfg import unitree_go2_ppo_runner_cfg
register_mjlab_task(
task_id="Unitree-Go2-Rough",
env_cfg=unitree_go2_rough_env_cfg(),
play_env_cfg=unitree_go2_rough_env_cfg(play=True),
rl_cfg=unitree_go2_ppo_runner_cfg(),
runner_cls=VelocityOnPolicyRunner,
)
register_mjlab_task(
task_id="Unitree-Go2-Flat",
env_cfg=unitree_go2_flat_env_cfg(),
play_env_cfg=unitree_go2_flat_env_cfg(play=True),
rl_cfg=unitree_go2_ppo_runner_cfg(),
runner_cls=VelocityOnPolicyRunner,
)
@@ -0,0 +1,170 @@
"""Unitree Go2 velocity environment configurations."""
from typing import Literal
from src.assets.robots import (
get_go2_robot_cfg,
)
from mjlab.envs import ManagerBasedRlEnvCfg
from mjlab.envs import mdp as envs_mdp
from mjlab.envs.mdp.actions import JointPositionActionCfg
from mjlab.managers import TerminationTermCfg
from mjlab.managers.event_manager import EventTermCfg
from mjlab.sensor import ContactMatch, ContactSensorCfg, RayCastSensorCfg
from mjlab.tasks.velocity import mdp
from mjlab.tasks.velocity.mdp import UniformVelocityCommandCfg
from src.tasks.velocity.velocity_env_cfg import make_velocity_env_cfg
TerrainType = Literal["rough", "obstacles"]
def unitree_go2_rough_env_cfg(
play: bool = False,
) -> ManagerBasedRlEnvCfg:
"""Create Unitree Go2 rough terrain velocity configuration."""
cfg = make_velocity_env_cfg()
cfg.sim.mujoco.ccd_iterations = 500
cfg.sim.contact_sensor_maxmatch = 500
cfg.scene.entities = {"robot": get_go2_robot_cfg()}
# Set raycast sensor frame to Go2 base_link.
for sensor in cfg.scene.sensors or ():
if sensor.name == "terrain_scan":
assert isinstance(sensor, RayCastSensorCfg)
sensor.frame.name = "base_link"
foot_names = ("FR", "FL", "RR", "RL")
site_names = ("FR", "FL", "RR", "RL")
geom_names = tuple(f"{name}_foot_collision" for name in foot_names)
feet_ground_cfg = ContactSensorCfg(
name="feet_ground_contact",
primary=ContactMatch(mode="geom", pattern=geom_names, entity="robot"),
secondary=ContactMatch(mode="body", pattern="terrain"),
fields=("found", "force"),
reduce="netforce",
num_slots=1,
track_air_time=True,
)
nonfoot_ground_cfg = ContactSensorCfg(
name="nonfoot_ground_touch",
primary=ContactMatch(
mode="geom",
entity="robot",
# Grab all collision geoms...
pattern=r".*_collision\d*$",
# Except for the foot geoms.
exclude=tuple(geom_names),
),
secondary=ContactMatch(mode="body", pattern="terrain"),
fields=("found", "force"),
reduce="none",
num_slots=1,
history_length=4,
)
cfg.scene.sensors = (cfg.scene.sensors or ()) + (
feet_ground_cfg,
nonfoot_ground_cfg,
)
if cfg.scene.terrain is not None and cfg.scene.terrain.terrain_generator is not None:
cfg.scene.terrain.terrain_generator.curriculum = True
joint_pos_action = cfg.actions["joint_pos"]
assert isinstance(joint_pos_action, JointPositionActionCfg)
cfg.viewer.body_name = "base_link"
cfg.viewer.distance = 1.5
cfg.viewer.elevation = -10.0
cfg.observations["critic"].terms["foot_height"].params["asset_cfg"].site_names = site_names
cfg.events["foot_friction"].params["asset_cfg"].geom_names = geom_names
cfg.events["base_com"].params["asset_cfg"].body_names = ("base_link",)
cfg.rewards["pose"].params["std_standing"] = {
r".*(FR|FL|RR|RL)_hip_joint.*": 0.05,
r".*(FR|FL|RR|RL)_thigh_joint.*": 0.1,
r".*(FR|FL|RR|RL)_calf_joint.*": 0.15,
}
cfg.rewards["pose"].params["std_walking"] = {
r".*(FR|FL|RR|RL)_hip_joint.*": 0.15,
r".*(FR|FL|RR|RL)_thigh_joint.*": 0.35,
r".*(FR|FL|RR|RL)_calf_joint.*": 0.5,
}
cfg.rewards["pose"].params["std_running"] = {
r".*(FR|FL|RR|RL)_hip_joint.*": 0.15,
r".*(FR|FL|RR|RL)_thigh_joint.*": 0.35,
r".*(FR|FL|RR|RL)_calf_joint.*": 0.5,
}
cfg.rewards["foot_gait"].params["offset"] = [0.0, 0.5, 0.5, 0.0]
cfg.rewards["body_orientation_l2"].params["asset_cfg"].body_names = ("base_link",)
cfg.rewards["body_ang_vel"].params["asset_cfg"].body_names = ("base_link",)
cfg.rewards["foot_clearance"].params["asset_cfg"].site_names = site_names
cfg.rewards["foot_slip"].params["asset_cfg"].site_names = site_names
cfg.terminations["illegal_contact"] = TerminationTermCfg(
func=mdp.illegal_contact,
params={"sensor_name": nonfoot_ground_cfg.name, "force_threshold": 10.0},
)
# Apply play mode overrides.
if play:
# Effectively infinite episode length.
cfg.episode_length_s = int(1e9)
cfg.observations["actor"].enable_corruption = False
cfg.events.pop("push_robot", None)
cfg.curriculum = {}
cfg.events["randomize_terrain"] = EventTermCfg(
func=envs_mdp.randomize_terrain,
mode="reset",
params={},
)
if cfg.scene.terrain is not None:
if cfg.scene.terrain.terrain_generator is not None:
cfg.scene.terrain.terrain_generator.curriculum = False
cfg.scene.terrain.terrain_generator.num_cols = 5
cfg.scene.terrain.terrain_generator.num_rows = 5
cfg.scene.terrain.terrain_generator.border_width = 10.0
return cfg
def unitree_go2_flat_env_cfg(play: bool = False) -> ManagerBasedRlEnvCfg:
"""Create Unitree Go2 flat terrain velocity configuration."""
cfg = unitree_go2_rough_env_cfg(play=play)
cfg.sim.njmax = 300
cfg.sim.mujoco.ccd_iterations = 50
cfg.sim.contact_sensor_maxmatch = 64
cfg.sim.nconmax = None
# Switch to flat terrain.
assert cfg.scene.terrain is not None
cfg.scene.terrain.terrain_type = "plane"
cfg.scene.terrain.terrain_generator = None
# Remove raycast sensor and height scan (no terrain to scan).
cfg.scene.sensors = tuple(
s for s in (cfg.scene.sensors or ()) if s.name != "terrain_scan"
)
del cfg.observations["actor"].terms["height_scan"]
del cfg.observations["critic"].terms["height_scan"]
# Disable terrain curriculum (not present in play mode since rough clears all).
cfg.curriculum.pop("terrain_levels", None)
if play:
twist_cmd = cfg.commands["twist"]
assert isinstance(twist_cmd, UniformVelocityCommandCfg)
twist_cmd.ranges.lin_vel_x = (-0.5, 1.0)
twist_cmd.ranges.lin_vel_y = (-0.5, 0.5)
twist_cmd.ranges.ang_vel_z = (-0.5, 0.5)
return cfg
@@ -0,0 +1,46 @@
"""RL configuration for Unitree Go2 velocity task."""
from mjlab.rl import (
RslRlModelCfg,
RslRlOnPolicyRunnerCfg,
RslRlPpoAlgorithmCfg,
)
def unitree_go2_ppo_runner_cfg() -> RslRlOnPolicyRunnerCfg:
"""Create RL runner configuration for Unitree Go2 velocity task."""
return RslRlOnPolicyRunnerCfg(
actor=RslRlModelCfg(
hidden_dims=(512, 256, 128),
activation="elu",
obs_normalization=True,
distribution_cfg={
"class_name": "GaussianDistribution",
"init_std": 1.0,
"std_type": "scalar",
},
),
critic=RslRlModelCfg(
hidden_dims=(512, 256, 128),
activation="elu",
obs_normalization=True,
),
algorithm=RslRlPpoAlgorithmCfg(
value_loss_coef=1.0,
use_clipped_value_loss=True,
clip_param=0.2,
entropy_coef=0.01,
num_learning_epochs=5,
num_mini_batches=4,
learning_rate=1.0e-3,
schedule="adaptive",
gamma=0.99,
lam=0.95,
desired_kl=0.01,
max_grad_norm=1.0,
),
experiment_name="go2_velocity",
save_interval=100,
num_steps_per_env=24,
max_iterations=10001,
)
@@ -0,0 +1,7 @@
from mjlab.envs.mdp import * # noqa: F401, F403
from .curriculums import * # noqa: F403
from .observations import * # noqa: F403
from .rewards import * # noqa: F403
from .terminations import * # noqa: F403
from .velocity_command import * # noqa: F403
@@ -0,0 +1,107 @@
from __future__ import annotations
from typing import TYPE_CHECKING, TypedDict, cast
import torch
from mjlab.entity import Entity
from mjlab.managers.scene_entity_config import SceneEntityCfg
from .velocity_command import UniformVelocityCommandCfg
if TYPE_CHECKING:
from mjlab.envs import ManagerBasedRlEnv
_DEFAULT_SCENE_CFG = SceneEntityCfg("robot")
class VelocityStage(TypedDict):
step: int
lin_vel_x: tuple[float, float] | None
lin_vel_y: tuple[float, float] | None
ang_vel_z: tuple[float, float] | None
class RewardWeightStage(TypedDict):
step: int
weight: float
def terrain_levels_vel(
env: ManagerBasedRlEnv,
env_ids: torch.Tensor,
command_name: str,
asset_cfg: SceneEntityCfg = _DEFAULT_SCENE_CFG,
) -> torch.Tensor:
asset: Entity = env.scene[asset_cfg.name]
terrain = env.scene.terrain
assert terrain is not None
terrain_generator = terrain.cfg.terrain_generator
assert terrain_generator is not None
command = env.command_manager.get_command(command_name)
assert command is not None
# Compute the distance the robot walked.
distance = torch.norm(
asset.data.root_link_pos_w[env_ids, :2] - env.scene.env_origins[env_ids, :2], dim=1
)
# Robots that walked far enough progress to harder terrains.
move_up = distance > terrain_generator.size[0] / 2
# Robots that walked less than half of their required distance go to simpler
# terrains.
move_down = (
distance < torch.norm(command[env_ids, :2], dim=1) * env.max_episode_length_s * 0.5
)
move_down *= ~move_up
# Update terrain levels.
terrain.update_env_origins(env_ids, move_up, move_down)
return torch.mean(terrain.terrain_levels.float())
def commands_vel(
env: ManagerBasedRlEnv,
env_ids: torch.Tensor,
command_name: str,
velocity_stages: list[VelocityStage],
) -> dict[str, torch.Tensor]:
del env_ids # Unused.
command_term = env.command_manager.get_term(command_name)
assert command_term is not None
cfg = cast(UniformVelocityCommandCfg, command_term.cfg)
for stage in velocity_stages:
if env.common_step_counter > stage["step"]:
if "lin_vel_x" in stage and stage["lin_vel_x"] is not None:
cfg.ranges.lin_vel_x = stage["lin_vel_x"]
if "lin_vel_y" in stage and stage["lin_vel_y"] is not None:
cfg.ranges.lin_vel_y = stage["lin_vel_y"]
if "ang_vel_z" in stage and stage["ang_vel_z"] is not None:
cfg.ranges.ang_vel_z = stage["ang_vel_z"]
return {
# "lin_vel_x_min": torch.tensor(cfg.ranges.lin_vel_x[0]),
# "lin_vel_x_max": torch.tensor(cfg.ranges.lin_vel_x[1]),
# "lin_vel_y_min": torch.tensor(cfg.ranges.lin_vel_y[0]),
# "lin_vel_y_max": torch.tensor(cfg.ranges.lin_vel_y[1]),
# "ang_vel_z_min": torch.tensor(cfg.ranges.ang_vel_z[0]),
# "ang_vel_z_max": torch.tensor(cfg.ranges.ang_vel_z[1]),
}
def reward_weight(
env: ManagerBasedRlEnv,
env_ids: torch.Tensor,
reward_name: str,
weight_stages: list[RewardWeightStage],
) -> torch.Tensor:
"""Update a reward term's weight based on training step stages."""
del env_ids # Unused.
reward_term_cfg = env.reward_manager.get_term_cfg(reward_name)
for stage in weight_stages:
if env.common_step_counter > stage["step"]:
reward_term_cfg.weight = stage["weight"]
return torch.tensor([reward_term_cfg.weight])
@@ -0,0 +1,54 @@
from __future__ import annotations
from typing import TYPE_CHECKING
import torch
from mjlab.entity import Entity
from mjlab.managers.scene_entity_config import SceneEntityCfg
from mjlab.sensor import ContactSensor
if TYPE_CHECKING:
from mjlab.envs import ManagerBasedRlEnv
_DEFAULT_ASSET_CFG = SceneEntityCfg("robot")
def foot_height(
env: ManagerBasedRlEnv, asset_cfg: SceneEntityCfg = _DEFAULT_ASSET_CFG
) -> torch.Tensor:
asset: Entity = env.scene[asset_cfg.name]
return asset.data.site_pos_w[:, asset_cfg.site_ids, 2] # (num_envs, num_sites)
def foot_air_time(env: ManagerBasedRlEnv, sensor_name: str) -> torch.Tensor:
sensor: ContactSensor = env.scene[sensor_name]
sensor_data = sensor.data
current_air_time = sensor_data.current_air_time
assert current_air_time is not None
return current_air_time
def foot_contact(env: ManagerBasedRlEnv, sensor_name: str) -> torch.Tensor:
sensor: ContactSensor = env.scene[sensor_name]
sensor_data = sensor.data
assert sensor_data.found is not None
return (sensor_data.found > 0).float()
def foot_contact_forces(env: ManagerBasedRlEnv, sensor_name: str) -> torch.Tensor:
sensor: ContactSensor = env.scene[sensor_name]
sensor_data = sensor.data
assert sensor_data.force is not None
forces_flat = sensor_data.force.flatten(start_dim=1) # [B, N*3]
return torch.sign(forces_flat) * torch.log1p(torch.abs(forces_flat))
def phase(env: ManagerBasedRlEnv, period: float, command_name: str) -> torch.Tensor:
global_phase = (env.episode_length_buf * env.step_dt) % period / period
phase = torch.zeros(env.num_envs, 2, device=env.device)
phase[:, 0] = torch.sin(global_phase * torch.pi * 2.0)
phase[:, 1] = torch.cos(global_phase * torch.pi * 2.0)
stand_mask = torch.linalg.norm(env.command_manager.get_command(command_name), dim=1) < 0.1
phase = torch.where(stand_mask.unsqueeze(1), torch.zeros_like(phase), phase)
return phase
@@ -0,0 +1,427 @@
from __future__ import annotations
from typing import TYPE_CHECKING
import torch
from mjlab.entity import Entity
from mjlab.managers.reward_manager import RewardTermCfg
from mjlab.managers.scene_entity_config import SceneEntityCfg
from mjlab.sensor import BuiltinSensor, ContactSensor
from mjlab.utils.lab_api.math import quat_apply_inverse
from mjlab.utils.lab_api.string import (
resolve_matching_names_values,
)
if TYPE_CHECKING:
from mjlab.envs import ManagerBasedRlEnv
_DEFAULT_ASSET_CFG = SceneEntityCfg("robot")
def track_linear_velocity(
env: ManagerBasedRlEnv,
std: float,
command_name: str,
asset_cfg: SceneEntityCfg = _DEFAULT_ASSET_CFG,
) -> torch.Tensor:
"""Reward for tracking the commanded base linear velocity.
The commanded z velocity is assumed to be zero.
"""
asset: Entity = env.scene[asset_cfg.name]
command = env.command_manager.get_command(command_name)
assert command is not None, f"Command '{command_name}' not found."
actual = asset.data.root_link_lin_vel_b
xy_error = torch.sum(torch.square(command[:, :2] - actual[:, :2]), dim=1)
z_error = torch.square(actual[:, 2])
lin_vel_error = xy_error + (2 * z_error)
return torch.exp(-lin_vel_error / std**2)
def track_angular_velocity(
env: ManagerBasedRlEnv,
std: float,
command_name: str,
asset_cfg: SceneEntityCfg = _DEFAULT_ASSET_CFG,
) -> torch.Tensor:
"""Reward heading error for heading-controlled envs, angular velocity for others.
The commanded xy angular velocities are assumed to be zero.
"""
asset: Entity = env.scene[asset_cfg.name]
command = env.command_manager.get_command(command_name)
assert command is not None, f"Command '{command_name}' not found."
actual = asset.data.root_link_ang_vel_b
z_error = torch.square(command[:, 2] - actual[:, 2])
xy_error = torch.sum(torch.square(actual[:, :2]), dim=1)
ang_vel_error = z_error + (0.05 * xy_error)
return torch.exp(-ang_vel_error / std**2)
def body_orientation_l2(
env: ManagerBasedRlEnv,
asset_cfg: SceneEntityCfg = _DEFAULT_ASSET_CFG,
) -> torch.Tensor:
"""Reward flat base orientation (robot being upright).
If asset_cfg has body_ids specified, computes the projected gravity
for that specific body. Otherwise, uses the root link projected gravity.
"""
asset: Entity = env.scene[asset_cfg.name]
# If body_ids are specified, compute projected gravity for that body.
if asset_cfg.body_ids:
body_quat_w = asset.data.body_link_quat_w[:, asset_cfg.body_ids, :] # [B, N, 4]
body_quat_w = body_quat_w.squeeze(1) # [B, 4]
gravity_w = asset.data.gravity_vec_w # [3]
projected_gravity_b = quat_apply_inverse(body_quat_w, gravity_w) # [B, 3]
xy_squared = torch.sum(torch.square(projected_gravity_b[:, :2]), dim=1)
else:
# Use root link projected gravity.
xy_squared = torch.sum(torch.square(asset.data.projected_gravity_b[:, :2]), dim=1)
return xy_squared
def self_collision_cost(
env: ManagerBasedRlEnv,
sensor_name: str,
force_threshold: float = 10.0,
) -> torch.Tensor:
"""Penalize self-collisions.
When the sensor provides force history (from ``history_length > 0``),
counts substeps where any contact force exceeds *force_threshold*.
Falls back to the instantaneous ``found`` count otherwise.
"""
sensor: ContactSensor = env.scene[sensor_name]
data = sensor.data
if data.force_history is not None:
# force_history: [B, N, H, 3]
force_mag = torch.norm(data.force_history, dim=-1) # [B, N, H]
hit = (force_mag > force_threshold).any(dim=1) # [B, H]
return hit.sum(dim=-1).float() # [B]
assert data.found is not None
return data.found.squeeze(-1)
def body_angular_velocity_penalty(
env: ManagerBasedRlEnv,
asset_cfg: SceneEntityCfg = _DEFAULT_ASSET_CFG,
) -> torch.Tensor:
"""Penalize excessive body angular velocities."""
asset: Entity = env.scene[asset_cfg.name]
ang_vel = asset.data.body_link_ang_vel_w[:, asset_cfg.body_ids, :]
ang_vel = ang_vel.squeeze(1)
ang_vel_xy = ang_vel[:, :2] # Don't penalize z-angular velocity.
return torch.sum(torch.square(ang_vel_xy), dim=1)
def angular_momentum_penalty(
env: ManagerBasedRlEnv,
sensor_name: str,
) -> torch.Tensor:
"""Penalize whole-body angular momentum to encourage natural arm swing."""
angmom_sensor: BuiltinSensor = env.scene[sensor_name]
angmom = angmom_sensor.data
angmom_magnitude_sq = torch.sum(torch.square(angmom), dim=-1)
angmom_magnitude = torch.sqrt(angmom_magnitude_sq)
env.extras["log"]["Metrics/angular_momentum_mean"] = torch.mean(angmom_magnitude)
return angmom_magnitude_sq
def feet_air_time(
env: ManagerBasedRlEnv,
sensor_name: str,
threshold: float = 0.4,
command_name: str | None = None,
command_threshold: float = 0.1,
) -> torch.Tensor:
"""Reward feet air time."""
sensor: ContactSensor = env.scene[sensor_name]
sensor_data = sensor.data
air_time = sensor_data.current_air_time
contact_time = sensor_data.current_contact_time
in_contact = contact_time > 0.0
in_mode_time = torch.where(in_contact, contact_time, air_time)
single_stance = torch.mean(in_contact.float(), dim=1) == 0.5
mode_time = torch.min(torch.where(single_stance.unsqueeze(-1), in_mode_time, 0.0), dim=1)[0]
error = torch.abs(mode_time - threshold)
reward = torch.clamp(threshold - error, min=0.0)
if command_name is not None:
command = env.command_manager.get_command(command_name)
if command is not None:
linear_norm = torch.norm(command[:, :2], dim=1)
angular_norm = torch.abs(command[:, 2])
total_command = linear_norm + angular_norm
scale = (total_command > command_threshold).float()
reward *= scale
return reward
def feet_clearance(
env: ManagerBasedRlEnv,
target_height: float,
command_name: str | None = None,
command_threshold: float = 0.1,
asset_cfg: SceneEntityCfg = _DEFAULT_ASSET_CFG,
) -> torch.Tensor:
"""Penalize deviation from target clearance height, weighted by foot velocity."""
asset: Entity = env.scene[asset_cfg.name]
foot_z = asset.data.site_pos_w[:, asset_cfg.site_ids, 2] # [B, N]
foot_vel_xy = asset.data.site_lin_vel_w[:, asset_cfg.site_ids, :2] # [B, N, 2]
vel_norm = torch.norm(foot_vel_xy, dim=-1) # [B, N]
delta = torch.abs(foot_z - target_height) # [B, N]
cost = torch.sum(delta * vel_norm, dim=1) # [B]
if command_name is not None:
command = env.command_manager.get_command(command_name)
if command is not None:
linear_norm = torch.norm(command[:, :2], dim=1)
angular_norm = torch.abs(command[:, 2])
total_command = linear_norm + angular_norm
active = (total_command > command_threshold).float()
cost = cost * active
return cost
def feet_gait(
env: ManagerBasedRlEnv,
period: float,
offset: list[float],
threshold: float,
command_threshold: float,
command_name: str,
sensor_name: str,
) -> torch.Tensor:
sensor: ContactSensor = env.scene[sensor_name]
is_contact = sensor.data.current_contact_time > 0
global_phase = ((env.episode_length_buf * env.step_dt) / period).unsqueeze(1)
offsets = torch.as_tensor(offset, device=env.device, dtype=global_phase.dtype).view(1, -1)
leg_phase = (global_phase + offsets) % 1.0
is_stance = (leg_phase < threshold)
reward = (is_stance == is_contact).float().mean(dim=1)
if command_name is not None:
command = env.command_manager.get_command(command_name)
if command is not None:
linear_norm = torch.norm(command[:, :2], dim=1)
angular_norm = torch.abs(command[:, 2])
total_command = linear_norm + angular_norm
scale = (total_command > command_threshold).float()
reward *= scale
return reward
class feet_swing_height:
"""Penalize deviation from target swing height, evaluated at landing."""
def __init__(self, cfg: RewardTermCfg, env: ManagerBasedRlEnv):
self.sensor_name = cfg.params["sensor_name"]
self.site_names = cfg.params["asset_cfg"].site_names
self.peak_heights = torch.zeros(
(env.num_envs, len(self.site_names)), device=env.device, dtype=torch.float32
)
self.step_dt = env.step_dt
def __call__(
self,
env: ManagerBasedRlEnv,
sensor_name: str,
target_height: float,
command_name: str,
command_threshold: float,
asset_cfg: SceneEntityCfg,
) -> torch.Tensor:
asset: Entity = env.scene[asset_cfg.name]
contact_sensor: ContactSensor = env.scene[sensor_name]
command = env.command_manager.get_command(command_name)
assert command is not None
foot_heights = asset.data.site_pos_w[:, asset_cfg.site_ids, 2]
in_air = contact_sensor.data.found == 0
self.peak_heights = torch.where(
in_air,
torch.maximum(self.peak_heights, foot_heights),
self.peak_heights,
)
first_contact = contact_sensor.compute_first_contact(dt=self.step_dt)
linear_norm = torch.norm(command[:, :2], dim=1)
angular_norm = torch.abs(command[:, 2])
total_command = linear_norm + angular_norm
active = (total_command > command_threshold).float()
error = self.peak_heights / target_height - 1.0
cost = torch.sum(torch.square(error) * first_contact.float(), dim=1) * active
num_landings = torch.sum(first_contact.float())
peak_heights_at_landing = self.peak_heights * first_contact.float()
mean_peak_height = torch.sum(peak_heights_at_landing) / torch.clamp(
num_landings, min=1
)
env.extras["log"]["Metrics/peak_height_mean"] = mean_peak_height
self.peak_heights = torch.where(
first_contact,
torch.zeros_like(self.peak_heights),
self.peak_heights,
)
return cost
def feet_slip(
env: ManagerBasedRlEnv,
sensor_name: str,
command_name: str,
command_threshold: float = 0.01,
asset_cfg: SceneEntityCfg = _DEFAULT_ASSET_CFG,
) -> torch.Tensor:
"""Penalize foot sliding (xy velocity while in contact)."""
asset: Entity = env.scene[asset_cfg.name]
contact_sensor: ContactSensor = env.scene[sensor_name]
command = env.command_manager.get_command(command_name)
assert command is not None
linear_norm = torch.norm(command[:, :2], dim=1)
angular_norm = torch.abs(command[:, 2])
total_command = linear_norm + angular_norm
active = (total_command > command_threshold).float()
assert contact_sensor.data.found is not None
in_contact = (contact_sensor.data.found > 0).float() # [B, N]
foot_vel_xy = asset.data.site_lin_vel_w[:, asset_cfg.site_ids, :2] # [B, N, 2]
vel_xy_norm = torch.norm(foot_vel_xy, dim=-1) # [B, N]
vel_xy_norm_sq = torch.square(vel_xy_norm) # [B, N]
cost = torch.sum(vel_xy_norm_sq * in_contact, dim=1) * active
num_in_contact = torch.sum(in_contact)
mean_slip_vel = torch.sum(vel_xy_norm * in_contact) / torch.clamp(
num_in_contact, min=1
)
env.extras["log"]["Metrics/slip_velocity_mean"] = mean_slip_vel
return cost
def soft_landing(
env: ManagerBasedRlEnv,
sensor_name: str,
command_name: str | None = None,
command_threshold: float = 0.05,
) -> torch.Tensor:
"""Penalize high impact forces at landing to encourage soft footfalls."""
contact_sensor: ContactSensor = env.scene[sensor_name]
sensor_data = contact_sensor.data
assert sensor_data.force is not None
forces = sensor_data.force # [B, N, 3]
force_magnitude = torch.norm(forces, dim=-1) # [B, N]
first_contact = contact_sensor.compute_first_contact(dt=env.step_dt) # [B, N]
landing_impact = force_magnitude * first_contact.float() # [B, N]
cost = torch.sum(landing_impact, dim=1) # [B]
num_landings = torch.sum(first_contact.float())
mean_landing_force = torch.sum(landing_impact) / torch.clamp(num_landings, min=1)
env.extras["log"]["Metrics/landing_force_mean"] = mean_landing_force
if command_name is not None:
command = env.command_manager.get_command(command_name)
if command is not None:
linear_norm = torch.norm(command[:, :2], dim=1)
angular_norm = torch.abs(command[:, 2])
total_command = linear_norm + angular_norm
active = (total_command > command_threshold).float()
cost = cost * active
return cost
class variable_posture:
"""Penalize deviation from default pose with speed-dependent tolerance.
Uses per-joint standard deviations to control how much each joint can deviate
from default pose. Smaller std = stricter (less deviation allowed), larger
std = more forgiving. The reward is: exp(-mean(error² / std²))
Three speed regimes (based on linear + angular command velocity):
- std_standing (speed < walking_threshold): Tight tolerance for holding pose.
- std_walking (walking_threshold <= speed < running_threshold): Moderate.
- std_running (speed >= running_threshold): Loose tolerance for large motion.
Tune std values per joint based on how much motion that joint needs at each
speed. Map joint name patterns to std values, e.g. {".*knee.*": 0.35}.
"""
def __init__(self, cfg: RewardTermCfg, env: ManagerBasedRlEnv):
asset: Entity = env.scene[cfg.params["asset_cfg"].name]
default_joint_pos = asset.data.default_joint_pos
assert default_joint_pos is not None
self.default_joint_pos = default_joint_pos
_, joint_names = asset.find_joints(cfg.params["asset_cfg"].joint_names)
_, _, std_standing = resolve_matching_names_values(
data=cfg.params["std_standing"],
list_of_strings=joint_names,
)
self.std_standing = torch.tensor(
std_standing, device=env.device, dtype=torch.float32
)
_, _, std_walking = resolve_matching_names_values(
data=cfg.params["std_walking"],
list_of_strings=joint_names,
)
self.std_walking = torch.tensor(std_walking, device=env.device, dtype=torch.float32)
_, _, std_running = resolve_matching_names_values(
data=cfg.params["std_running"],
list_of_strings=joint_names,
)
self.std_running = torch.tensor(std_running, device=env.device, dtype=torch.float32)
def __call__(
self,
env: ManagerBasedRlEnv,
std_standing,
std_walking,
std_running,
asset_cfg: SceneEntityCfg,
command_name: str,
walking_threshold: float = 0.5,
running_threshold: float = 1.5,
) -> torch.Tensor:
del std_standing, std_walking, std_running # Unused.
asset: Entity = env.scene[asset_cfg.name]
command = env.command_manager.get_command(command_name)
assert command is not None
linear_speed = torch.norm(command[:, :2], dim=1)
angular_speed = torch.abs(command[:, 2])
total_speed = linear_speed + angular_speed
standing_mask = (total_speed < walking_threshold).float()
walking_mask = (
(total_speed >= walking_threshold) & (total_speed < running_threshold)
).float()
running_mask = (total_speed >= running_threshold).float()
std = (
self.std_standing * standing_mask.unsqueeze(1)
+ self.std_walking * walking_mask.unsqueeze(1)
+ self.std_running * running_mask.unsqueeze(1)
)
current_joint_pos = asset.data.joint_pos[:, asset_cfg.joint_ids]
desired_joint_pos = self.default_joint_pos[:, asset_cfg.joint_ids]
error_squared = torch.square(current_joint_pos - desired_joint_pos)
return torch.exp(-torch.mean(error_squared / (std**2), dim=1))
def stand_still(
env: ManagerBasedRlEnv,
command_name: str,
command_threshold: float = 0.1,
asset_cfg: SceneEntityCfg = _DEFAULT_ASSET_CFG
) -> torch.Tensor:
asset: Entity = env.scene[asset_cfg.name]
diff_angle = asset.data.joint_pos[:, asset_cfg.joint_ids] - asset.data.default_joint_pos[:, asset_cfg.joint_ids]
reward = torch.sum(torch.square(diff_angle), dim=1)
if command_name is not None:
command = env.command_manager.get_command(command_name)
if command is not None:
linear_norm = torch.norm(command[:, :2], dim=1)
angular_norm = torch.abs(command[:, 2])
total_command = linear_norm + angular_norm
scale = (total_command <= command_threshold).float()
reward *= scale
return reward
@@ -0,0 +1,25 @@
from __future__ import annotations
from typing import TYPE_CHECKING
import torch
from mjlab.sensor import ContactSensor
if TYPE_CHECKING:
from mjlab.envs import ManagerBasedRlEnv
def illegal_contact(
env: ManagerBasedRlEnv,
sensor_name: str,
force_threshold: float = 10.0,
) -> torch.Tensor:
sensor: ContactSensor = env.scene[sensor_name]
data = sensor.data
if data.force_history is not None:
# force_history: [B, N, H, 3]
force_mag = torch.norm(data.force_history, dim=-1) # [B, N, H]
return (force_mag > force_threshold).any(dim=-1).any(dim=-1) # [B]
assert data.found is not None
return torch.any(data.found, dim=-1)
@@ -0,0 +1,282 @@
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass, field
from typing import TYPE_CHECKING
import numpy as np
import torch
from mjlab.entity import Entity
from mjlab.managers.command_manager import CommandTerm, CommandTermCfg
from mjlab.utils.lab_api.math import (
matrix_from_quat,
quat_apply,
wrap_to_pi,
)
if TYPE_CHECKING:
import viser
from mjlab.envs.manager_based_rl_env import ManagerBasedRlEnv
from mjlab.viewer.debug_visualizer import DebugVisualizer
class UniformVelocityCommand(CommandTerm):
cfg: UniformVelocityCommandCfg
def __init__(self, cfg: UniformVelocityCommandCfg, env: ManagerBasedRlEnv):
super().__init__(cfg, env)
if self.cfg.heading_command and self.cfg.ranges.heading is None:
raise ValueError("heading_command=True but ranges.heading is set to None.")
if self.cfg.ranges.heading and not self.cfg.heading_command:
raise ValueError("ranges.heading is set but heading_command=False.")
self.robot: Entity = env.scene[cfg.entity_name]
self.vel_command_b = torch.zeros(self.num_envs, 3, device=self.device)
self.heading_target = torch.zeros(self.num_envs, device=self.device)
self.heading_error = torch.zeros(self.num_envs, device=self.device)
self.is_heading_env = torch.zeros(
self.num_envs, dtype=torch.bool, device=self.device
)
self.is_standing_env = torch.zeros_like(self.is_heading_env)
self.metrics["error_vel_xy"] = torch.zeros(self.num_envs, device=self.device)
self.metrics["error_vel_yaw"] = torch.zeros(self.num_envs, device=self.device)
# Set by create_gui() when the viewer is active.
self._joystick_enabled: viser.GuiCheckboxHandle | None = None
self._joystick_sliders: list[viser.GuiSliderHandle] = []
self._joystick_get_env_idx: Callable[[], int] | None = None
@property
def command(self) -> torch.Tensor:
return self.vel_command_b
def _update_metrics(self) -> None:
max_command_time = self.cfg.resampling_time_range[1]
max_command_step = max_command_time / self._env.step_dt
self.metrics["error_vel_xy"] += (
torch.norm(
self.vel_command_b[:, :2] - self.robot.data.root_link_lin_vel_b[:, :2], dim=-1
)
/ max_command_step
)
self.metrics["error_vel_yaw"] += (
torch.abs(self.vel_command_b[:, 2] - self.robot.data.root_link_ang_vel_b[:, 2])
/ max_command_step
)
def _resample_command(self, env_ids: torch.Tensor) -> None:
r = torch.empty(len(env_ids), device=self.device)
self.vel_command_b[env_ids, 0] = r.uniform_(*self.cfg.ranges.lin_vel_x)
self.vel_command_b[env_ids, 1] = r.uniform_(*self.cfg.ranges.lin_vel_y)
self.vel_command_b[env_ids, 2] = r.uniform_(*self.cfg.ranges.ang_vel_z)
self.vel_command_b[env_ids, :] *= (torch.norm(self.vel_command_b[env_ids, :], dim=1) > 0.1).unsqueeze(1)
if self.cfg.heading_command:
assert self.cfg.ranges.heading is not None
self.heading_target[env_ids] = r.uniform_(*self.cfg.ranges.heading)
self.is_heading_env[env_ids] = r.uniform_(0.0, 1.0) <= self.cfg.rel_heading_envs
self.is_standing_env[env_ids] = r.uniform_(0.0, 1.0) <= self.cfg.rel_standing_envs
init_vel_mask = r.uniform_(0.0, 1.0) < self.cfg.init_velocity_prob
init_vel_env_ids = env_ids[init_vel_mask]
if len(init_vel_env_ids) > 0:
root_pos = self.robot.data.root_link_pos_w[init_vel_env_ids]
root_quat = self.robot.data.root_link_quat_w[init_vel_env_ids]
lin_vel_b = self.robot.data.root_link_lin_vel_b[init_vel_env_ids]
lin_vel_b[:, :2] = self.vel_command_b[init_vel_env_ids, :2]
root_lin_vel_w = quat_apply(root_quat, lin_vel_b)
root_ang_vel_b = self.robot.data.root_link_ang_vel_b[init_vel_env_ids]
root_ang_vel_b[:, 2] = self.vel_command_b[init_vel_env_ids, 2]
root_state = torch.cat(
[root_pos, root_quat, root_lin_vel_w, root_ang_vel_b], dim=-1
)
self.robot.write_root_state_to_sim(root_state, init_vel_env_ids)
def _update_command(self) -> None:
if self.cfg.heading_command:
self.heading_error = wrap_to_pi(self.heading_target - self.robot.data.heading_w)
env_ids = self.is_heading_env.nonzero(as_tuple=False).flatten()
self.vel_command_b[env_ids, 2] = torch.clip(
self.cfg.heading_control_stiffness * self.heading_error[env_ids],
min=self.cfg.ranges.ang_vel_z[0],
max=self.cfg.ranges.ang_vel_z[1],
)
standing_env_ids = self.is_standing_env.nonzero(as_tuple=False).flatten()
self.vel_command_b[standing_env_ids, :] = 0.0
# GUI.
def create_gui(
self,
name: str,
server: "viser.ViserServer",
get_env_idx: Callable[[], int],
) -> None:
"""Create velocity joystick sliders in the Viser viewer."""
from viser import Icon
ranges = self.cfg.ranges
axes = [
("lin_vel_x", ranges.lin_vel_x[1]),
("lin_vel_y", ranges.lin_vel_y[1]),
("ang_vel_z", ranges.ang_vel_z[1]),
]
sliders: list = []
with server.gui.add_folder(name.capitalize()):
enabled = server.gui.add_checkbox("Enable", initial_value=False)
for label, max_val in axes:
max_input = server.gui.add_slider(
f"Max {label}",
initial_value=max_val,
step=0.1,
min=0.1,
max=10.0,
)
slider = server.gui.add_slider(
label,
min=-max_val,
max=max_val,
step=0.05,
initial_value=0.0,
)
@max_input.on_update
def _(_ev, _s=slider, _m=max_input) -> None:
_s.min = -_m.value
_s.max = _m.value
sliders.append(slider)
zero_btn = server.gui.add_button("Zero", icon=Icon.SQUARE_X)
@zero_btn.on_click
def _(_) -> None:
for s in sliders:
s.value = 0.0
# Store GUI state for compute() override.
self._joystick_enabled = enabled
self._joystick_sliders = sliders
self._joystick_get_env_idx = get_env_idx
def compute(self, dt: float) -> None:
super().compute(dt)
if self._joystick_enabled is not None and self._joystick_enabled.value:
assert self._joystick_get_env_idx is not None
idx = self._joystick_get_env_idx()
for i, s in enumerate(self._joystick_sliders):
self.vel_command_b[idx, i] = s.value
# Visualization.
def _debug_vis_impl(self, visualizer: "DebugVisualizer") -> None:
"""Draw velocity command and actual velocity arrows."""
env_indices = visualizer.get_env_indices(self.num_envs)
if not env_indices:
return
cmds = self.command.cpu().numpy()
base_pos_ws = self.robot.data.root_link_pos_w.cpu().numpy()
base_quat_w = self.robot.data.root_link_quat_w
base_mat_ws = matrix_from_quat(base_quat_w).cpu().numpy()
lin_vel_bs = self.robot.data.root_link_lin_vel_b.cpu().numpy()
ang_vel_bs = self.robot.data.root_link_ang_vel_b.cpu().numpy()
scale = self.cfg.viz.scale
z_offset = self.cfg.viz.z_offset
for batch in env_indices:
base_pos_w = base_pos_ws[batch]
base_mat_w = base_mat_ws[batch]
cmd = cmds[batch]
lin_vel_b = lin_vel_bs[batch]
ang_vel_b = ang_vel_bs[batch]
# Skip if robot appears uninitialized (at origin).
if np.linalg.norm(base_pos_w) < 1e-6:
continue
# Helper to transform local to world coordinates.
def local_to_world(
vec: np.ndarray, pos: np.ndarray = base_pos_w, mat: np.ndarray = base_mat_w
) -> np.ndarray:
return pos + mat @ vec
# Command linear velocity arrow (blue).
cmd_lin_from = local_to_world(np.array([0, 0, z_offset]) * scale)
cmd_lin_to = local_to_world(
(np.array([0, 0, z_offset]) + np.array([cmd[0], cmd[1], 0])) * scale
)
visualizer.add_arrow(
cmd_lin_from, cmd_lin_to, color=(0.2, 0.2, 0.6, 0.6), width=0.015
)
# Command angular velocity arrow (green).
cmd_ang_from = cmd_lin_from
cmd_ang_to = local_to_world(
(np.array([0, 0, z_offset]) + np.array([0, 0, cmd[2]])) * scale
)
visualizer.add_arrow(
cmd_ang_from, cmd_ang_to, color=(0.2, 0.6, 0.2, 0.6), width=0.015
)
# Actual linear velocity arrow (cyan).
act_lin_from = local_to_world(np.array([0, 0, z_offset]) * scale)
act_lin_to = local_to_world(
(np.array([0, 0, z_offset]) + np.array([lin_vel_b[0], lin_vel_b[1], 0])) * scale
)
visualizer.add_arrow(
act_lin_from, act_lin_to, color=(0.0, 0.6, 1.0, 0.7), width=0.015
)
# Actual angular velocity arrow (light green).
act_ang_from = act_lin_from
act_ang_to = local_to_world(
(np.array([0, 0, z_offset]) + np.array([0, 0, ang_vel_b[2]])) * scale
)
visualizer.add_arrow(
act_ang_from, act_ang_to, color=(0.0, 1.0, 0.4, 0.7), width=0.015
)
@dataclass(kw_only=True)
class UniformVelocityCommandCfg(CommandTermCfg):
entity_name: str
heading_command: bool = False
heading_control_stiffness: float = 1.0
rel_standing_envs: float = 0.0
rel_heading_envs: float = 1.0
init_velocity_prob: float = 0.0
@dataclass
class Ranges:
lin_vel_x: tuple[float, float]
lin_vel_y: tuple[float, float]
ang_vel_z: tuple[float, float]
heading: tuple[float, float] | None = None
ranges: Ranges
@dataclass
class VizCfg:
z_offset: float = 0.2
scale: float = 0.5
viz: VizCfg = field(default_factory=VizCfg)
def build(self, env: ManagerBasedRlEnv) -> UniformVelocityCommand:
return UniformVelocityCommand(self, env)
def __post_init__(self):
if self.heading_command and self.ranges.heading is None:
raise ValueError(
"The velocity command has heading commands active (heading_command=True) but "
"the `ranges.heading` parameter is set to None."
)
@@ -0,0 +1,3 @@
from .runner import (
VelocityOnPolicyRunner as VelocityOnPolicyRunner,
)
@@ -0,0 +1,28 @@
import os
import wandb
from mjlab.rl import RslRlVecEnvWrapper
from mjlab.rl.exporter_utils import (
attach_metadata_to_onnx,
get_base_metadata,
)
from mjlab.rl.runner import MjlabOnPolicyRunner
class VelocityOnPolicyRunner(MjlabOnPolicyRunner):
env: RslRlVecEnvWrapper
def save(self, path: str, infos=None):
super().save(path, infos)
policy_path = path.split("model")[0]
filename = "policy.onnx"
self.export_policy_to_onnx(policy_path, filename)
run_name: str = (
wandb.run.name if self.logger.logger_type == "wandb" and wandb.run else "local"
) # type: ignore[assignment]
onnx_path = os.path.join(policy_path, filename)
metadata = get_base_metadata(self.env.unwrapped, run_name)
attach_metadata_to_onnx(onnx_path, metadata)
if self.logger.logger_type in ["wandb"]:
wandb.save(policy_path + filename, base_path=os.path.dirname(policy_path))
@@ -0,0 +1,431 @@
"""Velocity task configuration.
This module provides a factory function to create a base velocity task config.
Robot-specific configurations call the factory and customize as needed.
"""
import math
from dataclasses import replace
from mjlab.envs import ManagerBasedRlEnvCfg
from mjlab.envs import mdp as envs_mdp
from mjlab.envs.mdp import dr
from mjlab.envs.mdp.actions import JointPositionActionCfg
from mjlab.managers.action_manager import ActionTermCfg
from mjlab.managers.command_manager import CommandTermCfg
from mjlab.managers.curriculum_manager import CurriculumTermCfg
from mjlab.managers.event_manager import EventTermCfg
from mjlab.managers.metrics_manager import MetricsTermCfg
from mjlab.managers.observation_manager import ObservationGroupCfg, ObservationTermCfg
from mjlab.managers.reward_manager import RewardTermCfg
from mjlab.managers.scene_entity_config import SceneEntityCfg
from mjlab.managers.termination_manager import TerminationTermCfg
from mjlab.scene import SceneCfg
from mjlab.sensor import GridPatternCfg, ObjRef, RayCastSensorCfg
from mjlab.sim import MujocoCfg, SimulationCfg
from mjlab.tasks.velocity import mdp
from mjlab.tasks.velocity.mdp import UniformVelocityCommandCfg
from mjlab.terrains import TerrainEntityCfg
from mjlab.terrains.config import ROUGH_TERRAINS_CFG
from mjlab.utils.noise import UniformNoiseCfg as Unoise
from mjlab.viewer import ViewerConfig
import src.tasks.velocity.mdp as mdp
def make_velocity_env_cfg() -> ManagerBasedRlEnvCfg:
"""Create base velocity tracking task configuration."""
##
# Sensors
##
terrain_scan = RayCastSensorCfg(
name="terrain_scan",
frame=ObjRef(type="body", name="", entity="robot"), # Set per-robot.
ray_alignment="yaw",
pattern=GridPatternCfg(size=(1.6, 1.0), resolution=0.1),
max_distance=5.0,
exclude_parent_body=True,
debug_vis=True,
viz=RayCastSensorCfg.VizCfg(show_normals=True),
)
##
# Observations
##
actor_terms = {
"base_ang_vel": ObservationTermCfg(
func=mdp.builtin_sensor,
params={"sensor_name": "robot/imu_ang_vel"},
noise=Unoise(n_min=-0.2, n_max=0.2),
),
"projected_gravity": ObservationTermCfg(
func=mdp.projected_gravity,
noise=Unoise(n_min=-0.05, n_max=0.05),
),
"command": ObservationTermCfg(
func=mdp.generated_commands,
params={"command_name": "twist"},
),
"phase": ObservationTermCfg(
func=mdp.phase,
params={"period": 0.6, "command_name": "twist"},
),
"joint_pos": ObservationTermCfg(
func=mdp.joint_pos_rel,
noise=Unoise(n_min=-0.01, n_max=0.01),
),
"joint_vel": ObservationTermCfg(
func=mdp.joint_vel_rel,
noise=Unoise(n_min=-1.5, n_max=1.5),
),
"actions": ObservationTermCfg(func=mdp.last_action),
"height_scan": ObservationTermCfg(
func=envs_mdp.height_scan,
params={"sensor_name": "terrain_scan"},
noise=Unoise(n_min=-0.1, n_max=0.1),
scale=1 / terrain_scan.max_distance,
),
}
critic_terms = {
**actor_terms,
"base_lin_vel": ObservationTermCfg(
func=mdp.builtin_sensor,
params={"sensor_name": "robot/imu_lin_vel"},
noise=Unoise(n_min=-0.5, n_max=0.5),
),
"height_scan": ObservationTermCfg(
func=envs_mdp.height_scan,
params={"sensor_name": "terrain_scan"},
scale=1 / terrain_scan.max_distance,
),
"foot_height": ObservationTermCfg(
func=mdp.foot_height,
params={"asset_cfg": SceneEntityCfg("robot", site_names=())}, # Set per-robot.
),
"foot_air_time": ObservationTermCfg(
func=mdp.foot_air_time,
params={"sensor_name": "feet_ground_contact"},
),
"foot_contact": ObservationTermCfg(
func=mdp.foot_contact,
params={"sensor_name": "feet_ground_contact"},
),
"foot_contact_forces": ObservationTermCfg(
func=mdp.foot_contact_forces,
params={"sensor_name": "feet_ground_contact"},
),
}
observations = {
"actor": ObservationGroupCfg(
terms=actor_terms,
concatenate_terms=True,
enable_corruption=True,
history_length=1,
),
"critic": ObservationGroupCfg(
terms=critic_terms,
concatenate_terms=True,
enable_corruption=False,
history_length=1,
),
}
##
# Metrics
##
metrics = {
"mean_action_acc": MetricsTermCfg(
func=mdp.mean_action_acc,
),
}
##
# Actions
##
actions: dict[str, ActionTermCfg] = {
"joint_pos": JointPositionActionCfg(
entity_name="robot",
actuator_names=(".*",),
scale=0.25, # Override per-robot.
use_default_offset=True,
)
}
##
# Commands
##
commands: dict[str, CommandTermCfg] = {
"twist": UniformVelocityCommandCfg(
entity_name="robot",
resampling_time_range=(3.0, 8.0),
rel_standing_envs=0.05,
heading_command=True,
heading_control_stiffness=0.5,
debug_vis=True,
ranges=UniformVelocityCommandCfg.Ranges(
lin_vel_x=(-1.0, 2.0),
lin_vel_y=(-1.0, 1.0),
ang_vel_z=(-1.0, 1.0),
heading=(-math.pi, math.pi),
),
)
}
##
# Events
##
events = {
"reset_base": EventTermCfg(
func=mdp.reset_root_state_uniform,
mode="reset",
params={
"pose_range": {
"x": (-0.5, 0.5),
"y": (-0.5, 0.5),
"z": (0.0, 0.0),
"yaw": (-3.14, 3.14),
},
"velocity_range": {},
},
),
"reset_robot_joints": EventTermCfg(
func=mdp.reset_joints_by_offset,
mode="reset",
params={
"position_range": (-0.0, 0.0),
"velocity_range": (-0.0, 0.0),
"asset_cfg": SceneEntityCfg("robot", joint_names=(".*",)),
},
),
"push_robot": EventTermCfg(
func=mdp.push_by_setting_velocity,
mode="interval",
interval_range_s=(5.0, 6.0),
params={
"velocity_range": {
"x": (-0.5, 0.5),
"y": (-0.5, 0.5),
"z": (-0.4, 0.4),
"roll": (-0.52, 0.52),
"pitch": (-0.52, 0.52),
"yaw": (-0.78, 0.78),
},
},
),
"foot_friction": EventTermCfg(
mode="startup",
func=dr.geom_friction,
params={
"asset_cfg": SceneEntityCfg("robot", geom_names=()), # Set per-robot.
"operation": "abs",
"ranges": (0.3, 1.6),
"shared_random": True, # All foot geoms share the same friction.
},
),
"encoder_bias": EventTermCfg(
mode="startup",
func=dr.encoder_bias,
params={
"asset_cfg": SceneEntityCfg("robot"),
"bias_range": (-0.015, 0.015),
},
),
"base_com": EventTermCfg(
mode="startup",
func=dr.body_com_offset,
params={
"asset_cfg": SceneEntityCfg("robot", body_names=()), # Set per-robot.
"operation": "add",
"ranges": {
0: (-0.05, 0.05),
1: (-0.05, 0.05),
2: (-0.05, 0.05),
},
},
),
}
##
# Rewards
##
rewards = {
"track_linear_velocity": RewardTermCfg(
func=mdp.track_linear_velocity,
weight=1.0,
params={"command_name": "twist", "std": math.sqrt(0.25)},
),
"track_angular_velocity": RewardTermCfg(
func=mdp.track_angular_velocity,
weight=1.0,
params={"command_name": "twist", "std": math.sqrt(0.5)},
),
"body_orientation_l2": RewardTermCfg(
func=mdp.body_orientation_l2,
weight=-1.0,
params={"asset_cfg": SceneEntityCfg("robot", body_names=())}, # Set per-robot.
),
"pose": RewardTermCfg(
func=mdp.variable_posture,
weight=1.0,
params={
"asset_cfg": SceneEntityCfg("robot", joint_names=".*"),
"command_name": "twist",
"std_standing": {}, # Set per-robot.
"std_walking": {}, # Set per-robot.
"std_running": {}, # Set per-robot.
"walking_threshold": 0.1,
"running_threshold": 1.5,
},
),
"body_ang_vel": RewardTermCfg(
func=mdp.body_angular_velocity_penalty,
weight=-0.05, # Override per-robot
params={"asset_cfg": SceneEntityCfg("robot", body_names=())}, # Set per-robot.
),
"angular_momentum": RewardTermCfg(
func=mdp.angular_momentum_penalty,
weight=-0.025, # Override per-robot
params={"sensor_name": "robot/root_angmom"},
),
"is_terminated": RewardTermCfg(func=mdp.is_terminated, weight=-200.0),
"joint_acc_l2": RewardTermCfg(func=mdp.joint_acc_l2, weight=-2.5e-7),
"joint_pos_limits": RewardTermCfg(func=mdp.joint_pos_limits, weight=-10.0),
"action_rate_l2": RewardTermCfg(func=mdp.action_rate_l2, weight=-0.05),
"foot_gait": RewardTermCfg(
func=mdp.feet_gait,
weight=0.5,
params={
"period": 0.6,
"offset": [0.0, 0.5],
"threshold": 0.56,
"command_threshold": 0.1,
"command_name": "twist",
"sensor_name": "feet_ground_contact",
}
),
"foot_clearance": RewardTermCfg(
func=mdp.feet_clearance,
weight=-1.0,
params={
"target_height": 0.10,
"command_name": "twist",
"command_threshold": 0.1,
"asset_cfg": SceneEntityCfg("robot", site_names=()), # Set per-robot.
},
),
"foot_slip": RewardTermCfg(
func=mdp.feet_slip,
weight=-0.25,
params={
"sensor_name": "feet_ground_contact",
"command_name": "twist",
"command_threshold": 0.1,
"asset_cfg": SceneEntityCfg("robot", site_names=()), # Set per-robot.
},
),
"soft_landing": RewardTermCfg(
func=mdp.soft_landing,
weight=-1e-3,
params={
"sensor_name": "feet_ground_contact",
"command_name": "twist",
"command_threshold": 0.1,
},
),
"stand_still": RewardTermCfg(
func=mdp.stand_still,
weight=-1.0,
params={
"command_name": "twist",
"command_threshold": 0.1,
"asset_cfg": SceneEntityCfg("robot", joint_names=".*"),
},
),
}
##
# Terminations
##
terminations = {
"time_out": TerminationTermCfg(func=mdp.time_out, time_out=True),
"fell_over": TerminationTermCfg(
func=mdp.bad_orientation,
params={"limit_angle": math.radians(70.0)},
),
}
##
# Curriculum
##
curriculum = {
"terrain_levels": CurriculumTermCfg(
func=mdp.terrain_levels_vel,
params={"command_name": "twist"},
),
"command_vel": CurriculumTermCfg(
func=mdp.commands_vel,
params={
"command_name": "twist",
"velocity_stages": [
{"step": 0, "lin_vel_x": (-0.5, 1.0), "lin_vel_y": (-0.5, 0.5), "ang_vel_z": (-1.0, 1.0)},
{"step": 5000 * 24, "lin_vel_x": (-1.0, 2.0), "lin_vel_y": (-1.0, 1.0)},
],
},
),
}
##
# Assemble and return
##
return ManagerBasedRlEnvCfg(
scene=SceneCfg(
terrain=TerrainEntityCfg(
terrain_type="generator",
terrain_generator=replace(ROUGH_TERRAINS_CFG),
max_init_terrain_level=5,
),
sensors=(terrain_scan,),
num_envs=1,
extent=2.0,
),
observations=observations,
actions=actions,
commands=commands,
events=events,
rewards=rewards,
terminations=terminations,
curriculum=curriculum,
metrics=metrics,
viewer=ViewerConfig(
origin_type=ViewerConfig.OriginType.ASSET_BODY,
entity_name="robot",
body_name="", # Set per-robot.
distance=3.0,
elevation=-5.0,
azimuth=90.0,
),
sim=SimulationCfg(
nconmax=35,
njmax=1500,
mujoco=MujocoCfg(
timestep=0.005,
iterations=10,
ls_iterations=20,
),
),
decimation=4,
episode_length_s=20.0,
)
+5 -6
View File
@@ -25,7 +25,7 @@ from pathlib import Path
from typing import Any from typing import Any
from urllib.parse import unquote, urlsplit from urllib.parse import unquote, urlsplit
VERSION = "0.2.0" VERSION = "0.3.0"
# 浏览器当前 ONNX 运行时只实现 Go2 的 47→12 部署契约;其他任务须由服务启动参数显式放行。 # 浏览器当前 ONNX 运行时只实现 Go2 的 47→12 部署契约;其他任务须由服务启动参数显式放行。
DEFAULT_TASKS = ("Unitree-Go2-Flat",) DEFAULT_TASKS = ("Unitree-Go2-Flat",)
ACTIVE_STATES = {"queued", "running"} ACTIVE_STATES = {"queued", "running"}
@@ -130,7 +130,7 @@ class TrainingManager:
if self.check_environment and self._environment_error is False: if self.check_environment and self._environment_error is False:
probe = ( probe = (
"import importlib.util,sys; " "import importlib.util,sys; "
"missing=[m for m in ('mjlab','torch','tyro') " "missing=[m for m in ('mjlab','torch','tyro','wandb') "
"if importlib.util.find_spec(m) is None]; " "if importlib.util.find_spec(m) is None]; "
"print(','.join(missing)); sys.exit(bool(missing))" "print(','.join(missing)); sys.exit(bool(missing))"
) )
@@ -414,7 +414,7 @@ class TrainingRequestHandler(BaseHTTPRequestHandler):
manager: TrainingManager manager: TrainingManager
allowed_origins: tuple[str, ...] = () allowed_origins: tuple[str, ...] = ()
access_token = "" access_token = ""
server_version = "MuJoCoLocalTraining/0.2" server_version = "MuJoCoLocalTraining/0.3"
def log_message(self, format: str, *args: Any) -> None: def log_message(self, format: str, *args: Any) -> None:
sys.stderr.write(f"[{self.log_date_time_string()}] {format % args}\n") sys.stderr.write(f"[{self.log_date_time_string()}] {format % args}\n")
@@ -553,8 +553,7 @@ def default_trainer_root() -> Path:
configured = os.environ.get("UNITREE_RL_MJLAB_ROOT") configured = os.environ.get("UNITREE_RL_MJLAB_ROOT")
if configured: if configured:
return Path(configured) return Path(configured)
repository = Path(__file__).resolve().parents[2] return Path(__file__).resolve().parent / "rl"
return repository.parent.parent / "unitree_rl_mjlab"
def parse_args() -> argparse.Namespace: def parse_args() -> argparse.Namespace:
@@ -570,7 +569,7 @@ def parse_args() -> argparse.Namespace:
"--trainer-root", "--trainer-root",
type=Path, type=Path,
default=default_trainer_root(), default=default_trainer_root(),
help="unitree_rl_mjlab 工程目录", help="训练工程目录;默认使用仓库内置的 Go2 训练器",
) )
parser.add_argument( parser.add_argument(
"--trainer-python", default=sys.executable, help="已安装 mjlab/torch 的 Python 解释器" "--trainer-python", default=sys.executable, help="已安装 mjlab/torch 的 Python 解释器"
+17
View File
@@ -14,6 +14,7 @@ from server import ( # noqa: E402
TrainingJob, TrainingJob,
TrainingManager, TrainingManager,
TrainingRequestHandler, TrainingRequestHandler,
default_trainer_root,
termination_signal_handler, termination_signal_handler,
) )
@@ -57,6 +58,22 @@ out.write_bytes(b'onnx')
value.update(patch) value.update(patch)
return value return value
def test_default_trainer_is_bundled_with_go2_assets(self):
trainer_root = default_trainer_root()
self.assertEqual(trainer_root, Path(__file__).resolve().parents[1] / "rl")
self.assertTrue((trainer_root / "scripts" / "train.py").is_file())
self.assertTrue(
(
trainer_root
/ "src"
/ "assets"
/ "robots"
/ "unitree_go2"
/ "xmls"
/ "go2.xml"
).is_file()
)
def test_validates_allowlist_and_limits(self): def test_validates_allowlist_and_limits(self):
with self.assertRaises(ApiError): with self.assertRaises(ApiError):
self.manager.parse_config(self.payload(taskId="shell injection")) self.manager.parse_config(self.payload(taskId="shell injection"))
+2 -3
View File
@@ -112,17 +112,16 @@ Python 控制器是可信的单文件脚本,必须同步定义 `step(ctx, stat
## 本地强化学习训练 ## 本地强化学习训练
训练仍由本机 Python/mjlab 进程执行,但可以从右侧“控制 → 本地强化学习训练”直接发起和管理。先使用安装了 mjlab、PyTorch 及训练依赖的 Python 启动本地桥接服务: 训练仍由本机 Python/mjlab 进程执行,但可以从右侧“控制 → 本地强化学习训练”直接发起和管理。仓库已经内置默认 Go2 任务的训练代码与资产;先按 [`training_server/README.md`](../training_server/README.md) 安装训练依赖,再使用对应 Python 启动本地桥接服务:
```bash ```bash
npm run training-server -- \ npm run training-server -- \
--trainer-root /path/to/unitree_rl_mjlab \
--trainer-python /path/to/training-env/bin/python --trainer-python /path/to/training-env/bin/python
``` ```
服务启动时会在终端输出一个随机访问令牌;在界面中填写该令牌后连接。令牌仅保存在当前标签页的 `sessionStorage`。界面默认连接 `http://127.0.0.1:8765`,可选择服务端允许的任务、并行环境数、训练迭代、随机种子、CPU/GPU、GPU 编号和实验记录方式。W&B 默认为本地离线模式,无需登录或 API Key;也可完全禁用,只有明确选择在线模式时才会联网登录。训练期间页面轮询迭代进度与最近日志,可以停止任务;训练成功后点击“导入策略”,生成的 `policy.onnx` 会进入现有 ONNX 加载流程。 服务启动时会在终端输出一个随机访问令牌;在界面中填写该令牌后连接。令牌仅保存在当前标签页的 `sessionStorage`。界面默认连接 `http://127.0.0.1:8765`,可选择服务端允许的任务、并行环境数、训练迭代、随机种子、CPU/GPU、GPU 编号和实验记录方式。W&B 默认为本地离线模式,无需登录或 API Key;也可完全禁用,只有明确选择在线模式时才会联网登录。训练期间页面轮询迭代进度与最近日志,可以停止任务;训练成功后点击“导入策略”,生成的 `policy.onnx` 会进入现有 ONNX 加载流程。
桥接服务只监听本机回环地址,并检查 Host、Origin 和 Bearer Token;仅接受允许列表中的任务和经过范围校验的参数,不执行前端提供的 Shell 命令;一次只运行一个训练进程。当前任务使用 `unitree_rl_mjlab` 自带的机器人资产与环境配置,**不会自动把浏览器中临时编辑的 MJCF/URDF 作为训练环境**。自定义浏览器模型训练需要先在 mjlab 中注册对应 task。服务配置、接口和安全边界见 [`../training_server/README.md`](../training_server/README.md)。 桥接服务只监听本机回环地址,并检查 Host、Origin 和 Bearer Token;仅接受允许列表中的任务和经过范围校验的参数,不执行前端提供的 Shell 命令;一次只运行一个训练进程。默认任务使用仓库内置的 Go2 机器人资产与环境配置,**不会自动把浏览器中临时编辑的 MJCF/URDF 作为训练环境**。自定义浏览器模型训练需要在兼容的外部训练工程中注册 task,并通过服务的 `--trainer-root` 指定该工程。服务配置、接口和安全边界见 [`../training_server/README.md`](../training_server/README.md)。
## ONNX 强化学习策略 ## ONNX 强化学习策略