chore: release v0.0.1

This commit is contained in:
motphys-developers
2025-11-20 08:57:48 +00:00
commit 5133830b5a
105 changed files with 8789 additions and 0 deletions

8
.gitattributes vendored Normal file
View File

@@ -0,0 +1,8 @@
* text=auto
*.sh text eol=lf
*.stl filter=lfs diff=lfs merge=lfs -text
*.obj filter=lfs diff=lfs merge=lfs -text
*.pickle filter=lfs diff=lfs merge=lfs -text
*.png filter=lfs diff=lfs merge=lfs -text
*.mp4 filter=lfs diff=lfs merge=lfs -text
*.jpg filter=lfs diff=lfs merge=lfs -text

11
.gitignore vendored Normal file
View File

@@ -0,0 +1,11 @@
__pycache__
runs
.pdm-python
logs
.idea
.claude
.venv
node_modules
docs/build
docs/source/user_guide
docs/source/index.md

10
.prettierignore Normal file
View File

@@ -0,0 +1,10 @@
.gitlab/ci/release.json
CHANGELOG.md
package-lock.json
packages-lock.json
TableOfContents.md
node_modules
.venv
.gitlab-ci.yml
docs/build/
.readthedocs.yaml

13
.prettierrc Normal file
View File

@@ -0,0 +1,13 @@
trailingComma: "es5"
tabWidth: 4
semi: true
singleQuote: false
printWidth: 120
endOfLine: "auto"
overrides:
- files: "*.yml"
options:
tabWidth: 2
- files: "*.json"
options:
tabWidth: 2

1
.python-version Normal file
View File

@@ -0,0 +1 @@
3.10

34
.readthedocs.yaml Normal file
View File

@@ -0,0 +1,34 @@
version: 2
build:
os: ubuntu-22.04
tools:
python: "3.10"
jobs:
post_checkout:
- mkdir -p tools
# Download and uncompress the binary
# https://git-lfs.github.com/
- wget https://github.com/git-lfs/git-lfs/releases/download/v3.1.4/git-lfs-linux-amd64-v3.1.4.tar.gz -P tools
- tar xvfz ./tools/git-lfs-linux-amd64-v3.1.4.tar.gz -C tools
# Modify LFS config paths to point where git-lfs binary was downloaded
- git config filter.lfs.process "`pwd`/tools/git-lfs filter-process"
- git config filter.lfs.smudge "`pwd`/tools/git-lfs smudge -- %f"
- git config filter.lfs.clean "`pwd`/tools/git-lfs clean -- %f"
# Make LFS available in current repository
- ./tools/git-lfs install
# Download content from remote
- ./tools/git-lfs fetch
# Make local files to have the real content on them
- ./tools/git-lfs checkout
- rm -rf ./tools
sphinx:
configuration: docs/source/conf.py
python:
install:
- method: pip
path: .
extra_requirements:
- docs

125
CLAUDE.md Normal file
View File

@@ -0,0 +1,125 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
MotrixLab is a reinforcement learning framework built on top of MotrixSim simulation backend. It provides a unified interface for training RL agents using multiple simulation backends (MotrixSim) and primarily integrates with the SKRL library. The framework is designed for robotics simulation and supports various environments including basic cartpole, locomotion tasks, and manipulation tasks.
## Development Setup
This project uses UV for dependency management and Python 3.10.
### Installation
```bash
uv sync --all-packages --all-extras
```
For SKRL framework with specific backend:
```bash
uv sync --all-packages --extra skrl-jax # JAX backend
uv sync --all-packages --extra skrl-torch # PyTorch backend
```
**Available dependency groups in MotrixLab:**
- `skrl-jax`: SKRL RL framework with JAX backend
- `skrl-torch`: SKRL RL framework with PyTorch backend
- `test`: Test dependencies (pytest)
**Note**: This is a workspace project with two main packages: `motrix_envs` (simulation environments) and `motrix_rl` (RL framework integration).
## Common Commands
### Training
```bash
uv run scripts/train.py --env cartpole
```
### Environment Visualization
View environment without training:
```bash
uv run scripts/view.py --env cartpole
```
### Playing/Evaluation
```bash
uv run scripts/play.py --env cartpole
```
Specify policy file:
```bash
uv run scripts/play.py --env cartpole --policy <path/to/best.[pickle/pt]>
```
### Rendering
Add `--render` flag to training for visualization:
```bash
uv run scripts/train.py --env cartpole --render
```
### TensorBoard
```bash
uv run tensorboard --logdir runs/{env-name}
```
### Testing
```bash
uv run pytest
```
## Architecture
### Core Components
1. **Workspace Structure**:
- `motrix_envs/`: Simulation environment definitions using MotrixSim backend
- `motrix_rl/`: RL framework integration (primarily SKRL) and training utilities
2. **Scripts** (`scripts/`):
- `train.py`: Main training script with configurable environments and backends
- `view.py`: Environment visualization without training
- `play.py`: Policy evaluation and testing
3. **Environment Registry**: Environments are registered via `motrix_envs.registry` and accessed using string names like "cartpole"
### Key Architecture Points
- **Workspace Project**: Uses UV workspace with two packages sharing dependencies
- **MotrixSim Backend**: Built on MotrixSim simulation engine for physics simulation
- **SKRL Integration**: Primary RL framework supporting both JAX and PyTorch backends
- **Environment Naming**: Simple string-based environment identification (e.g., "cartpole")
- **Automatic Backend Selection**: Training script automatically selects JAX or PyTorch based on GPU availability
- **Multi-Backend Training**: Supports different simulation backends for the same environment
### Environment Usage Pattern
```python
# Environment creation handled internally by scripts
# Use string names to specify environments
uv run scripts/train.py --env cartpole
```
## Results Storage
Training results are saved to `runs/{env-name}/` directory structure with checkpoints and tensorboard logs.
## Important Notes
- **Python Version**: Requires exactly Python 3.10.\*
- **GPU Support**: Includes CUDA support for both JAX and PyTorch backends
- **Private PyPI**: Uses internal PyPI server for MotrixSim packages
- **No Manual Tests**: No test files found in the repository structure

202
LICENSE Normal file
View File

@@ -0,0 +1,202 @@
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 [yyyy] [name of copyright owner]
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.

14
NOTICE Normal file
View File

@@ -0,0 +1,14 @@
Copyright (C) 2020-2025 Motphys Technology Co., Ltd. All Rights Reserved.
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.
==============================================================================

100
README.md Normal file
View File

@@ -0,0 +1,100 @@
# MotrixLab
![GitHub License](https://img.shields.io/github/license/Motphys/MotrixLab)
![Python Version](https://img.shields.io/badge/python-3.10-blue)
`MotrixLab` 是一个基于 [MotrixSim](https://github.com/Motphys/motrixsim-docs) 仿真引擎的强化学习框架,专为机器人仿真和训练设计。该项目提供了一个完整的强化学习开发平台,集成了多种仿真环境和训练框架。
## 项目概述
该项目分为两个核心部分:
- **motrix_envs**: 基于 MotrixSim 构建的各种 RL 仿真环境,定义了 observation、action、reward。与具体的 RL 框架无关,目前支持 MotrixSim 的 CPU 后端
- **motrix_rl**: 集成 RL 框架,并使用 motrix_envs 里的各种环境参数进行训练。目前支持 SKRL 框架的 PPO 算法
> 文档地址https://motrixlab.readthedocs.io
## 主要特性
- **统一接口**: 提供简洁统一的强化学习训练和评估接口
- **多后端支持**: 支持 JAX 和 PyTorch 训练后端,可根据硬件环境灵活选择
- **丰富环境**: 包含基础控制、运动、操作等多种机器人仿真环境
- **高性能仿真**: 基于 MotrixSim 的高性能物理仿真引擎
- **可视化训练**: 支持实时渲染和训练过程可视化
## 🚀 快速开始
> 以下示例使用了 Python 项目管理工具:[UV](https://docs.astral.sh/uv/)
>
> 在开始之前,请先[安装](https://docs.astral.sh/uv/getting-started/installation/)该工具。
### 克隆仓库
```bash
git clone https://github.com/Motphys/MotrixLab
cd MotrixLab
git lfs pull
```
### 安装依赖
安装全部依赖:
```bash
uv sync --all-packages --all-extras
```
SKRL 框架支持 JAX(Flax)或 PyTorch 作为训练后端,您也可以根据自己的设备环境,选择只安装其中一种训练后端:
安装 JAX 作为训练后端(仅支持 Linux 平台):
```bash
uv sync --all-packages --extra skrl-jax
```
安装 PyTorch 作为训练后端:
```bash
uv sync --all-packages --extra skrl-torch
```
## 🎯 使用指南
### 环境可视化
查看环境而不执行训练:
```bash
uv run scripts/view.py --env cartpole
```
### 训练模型
```bash
uv run scripts/train.py --env cartpole
```
训练结果会保存在 `runs/{env-name}/` 目录下。
通过 TensorBoard 查看训练数据:
```bash
uv run tensorboard --logdir runs/{env-name}
```
### 模型推理
```
uv run scripts/play.py --env cartpole
```
更多使用方式请参考[用户文档](https://motrixlab.readthedocs.io)
## 📬 联系方式
有问题或建议?欢迎通过以下方式联系我们:
- GitHub Issues: [提交问题](https://github.com/Motphys/MotrixLab/issues)
- Discussions: [加入讨论](https://github.com/Motphys/MotrixLab/discussions)

142
docs/CLAUDE.md Normal file
View File

@@ -0,0 +1,142 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
This is the documentation project for MotrixSim, a high-performance physics simulation engine for multibody dynamics and robotics simulation. The documentation is built using Sphinx and targets both Chinese and English audiences.
## Key Commands
### Building Documentation
- **Build HTML**: `make html` or `sphinx-build -b html source build/html`
- **Clean build**: `make clean` followed by `make html`
- **Serve locally**: `python -m http.server 8000 -d build/html`
- **Watch mode**: Use `sphinx-autobuild source build/html --host 0.0.0.0 --port 8000`
### Development Tools
- **Check links**: `make linkcheck`
- **Build PDF**: `make latexpdf` (requires LaTeX)
- **Build single HTML**: `make singlehtml`
- **Build EPUB**: `make epub`
## Documentation Structure
### Source Organization
- **source/**: Main documentation source files
- **index.md**: Landing page with project overview and videos
- **user_guide/**: User documentation and tutorials
- **api_reference/**: API documentation and reference
- **en/**: English language content
- **zh_CN/**: Chinese language content
- **\_static/**: Static assets (images, videos, CSS, JS)
### User Guide Structure
- **getting_started/**: Installation and quick start guides
- **overview/**: Project overview and comparisons
- **kinematics/**: Kinematics documentation (joints, bodies, sensors, etc.)
- **main_function/**: Core functionality documentation
- **render/**: Rendering and visualization documentation
### API Reference Structure
- **core/**: Core API modules
- **ik/**: Inverse kinematics API
- **rendering/**: Rendering and visualization API
- **low/**: Low-level API (excluded from main documentation)
## Build Configuration
### Sphinx Configuration (conf.py)
- **Theme**: PyData Sphinx Theme with custom branding
- **Extensions**:
- sphinx.ext.autodoc (API documentation from docstrings)
- sphinx.ext.napoleon (NumPy/Google style docstrings)
- sphinx.ext.autosummary (automated summaries)
- myst_parser (Markdown support)
- sphinx-design (design components)
- sphinx_copybutton (code copy functionality)
- **Language Support**: Chinese (zh_CN) and English with automatic content copying
- **Autodoc**: Automatic API documentation generation from Python docstrings
### Dependencies
Documentation dependencies are managed in the parent `pyproject.toml`:
```toml
docs = [
"sphinx",
"autodocsumm",
"pydata-sphinx-theme",
"myst-parser",
"sphinx-copybutton",
"sphinx-subfigure",
"sphinxcontrib-video",
"sphinx-togglebutton",
"sphinx-design",
]
```
## Content Guidelines
### Documentation Style
- **Primary Language**: Chinese (zh_CN) with English translations
- **Format**: Markdown (.md) files using MyST parser
- **Code Examples**: Include practical code examples with proper syntax highlighting
- **API Documentation**: Use NumPy/Google style docstrings for autodoc generation
### Media and Assets
- **Videos**: Embedded using sphinxcontrib-video extension
- **Images**: Stored in `_static/images/` with poster images for videos
- **Static Files**: Custom CSS and JavaScript in `_static/` directory
- **Logos**: Light and dark theme variants available
### Multilingual Support
- **Default Language**: Chinese (zh_CN)
- **Content Copying**: Automatic copying of language-specific content during build
- **Theme**: Supports both light and dark themes with appropriate logos
## Development Workflow
### Adding New Documentation
1. Create Markdown files in appropriate directory (user_guide/ or api_reference/)
2. Follow existing structure and naming conventions
3. Update table of contents in relevant index.md files
4. Test build locally before committing
### API Documentation
1. Write docstrings in Python source code using NumPy/Google style
2. Use autodoc directives to generate API documentation
3. Organize API docs by functional modules
4. Include examples and usage notes
### Building and Testing
1. Always build locally before committing: `make html`
2. Check for broken links: `make linkcheck`
3. Verify multilingual content works correctly
4. Test all embedded media and interactive elements
## File Naming Conventions
- Use lowercase with underscores for file names
- Index files should be named `index.md`
- API documentation files should match module names
- Media files should be descriptive and use appropriate extensions
## Integration Notes
- This documentation project is part of the larger motrixsim-py package
- The build system automatically detects and documents the motrixsim package
- Documentation is deployed to ReadTheDocs at https://motrixsim.readthedocs.io
- Source code is hosted at https://github.com/Motphys/motrixsim-docs

20
docs/Makefile Normal file
View File

@@ -0,0 +1,20 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = source
BUILDDIR = build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

88
docs/gen_poster.sh Executable file
View File

@@ -0,0 +1,88 @@
#!/bin/bash
# Poster generation script for videos
# Extracts the first frame from all videos in source/_static/videos/
# and saves them as poster images in source/_static/images/poster/
set -e
# Define directories
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SOURCE_DIR="$SCRIPT_DIR/source/_static/videos"
TARGET_DIR="$SCRIPT_DIR/source/_static/images/poster"
# Supported video extensions
VIDEO_EXTENSIONS=("mp4" "avi" "mov" "mkv" "webm" "flv" "wmv")
# Check if ffmpeg is installed
if ! command -v ffmpeg &> /dev/null; then
echo "Error: ffmpeg is not installed. Please install ffmpeg first."
echo "On Ubuntu/Debian: sudo apt-get install ffmpeg"
echo "On macOS: brew install ffmpeg"
exit 1
fi
# Check if source directory exists
if [ ! -d "$SOURCE_DIR" ]; then
echo "Error: Source directory '$SOURCE_DIR' does not exist."
exit 1
fi
# Create target directory if it doesn't exist
mkdir -p "$TARGET_DIR"
echo "Created target directory: $TARGET_DIR"
# Counter for processed files
processed=0
skipped=0
# Process all video files
echo "Processing videos in: $SOURCE_DIR"
echo "------------------------------------------------"
for video_file in "$SOURCE_DIR"/*; do
# Check if file exists and is a regular file
[ -f "$video_file" ] || continue
# Get file extension
filename=$(basename "$video_file")
extension="${filename##*.}"
filename_noext="${filename%.*}"
# Check if file has supported video extension
if [[ " ${VIDEO_EXTENSIONS[*]} " =~ " ${extension,,} " ]]; then
output_file="$TARGET_DIR/${filename_noext}.jpg"
# Check if output file already exists
if [ -f "$output_file" ]; then
echo "<EFBFBD> Skipping '$filename' (poster already exists)"
((skipped++))
continue
fi
echo "<<3C> Processing '$filename'..."
# Extract first frame using ffmpeg
if ffmpeg -i "$video_file" -vframes 1 -q:v 2 "$output_file" -y -loglevel error; then
echo " Created poster: ${filename_noext}.jpg"
((processed++))
else
echo "L Failed to process '$filename'"
rm -f "$output_file" # Remove any partial output
fi
else
echo "<EFBFBD> Skipping '$filename' (not a supported video format)"
((skipped++))
fi
done
echo "------------------------------------------------"
echo "Poster generation completed!"
echo "=<3D> Summary:"
echo " Processed: $processed videos"
echo " Skipped: $skipped files"
echo " Posters saved to: $TARGET_DIR"
if [ $processed -eq 0 ]; then
echo "<22> No new posters were created."
fi

35
docs/make.bat Normal file
View File

@@ -0,0 +1,35 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=source
set BUILDDIR=build
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.https://www.sphinx-doc.org/
exit /b 1
)
if "%1" == "" goto help
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
:end
popd

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<polygon points="44.76 37.22 26.34 37.22 22.32 42.33 48.75 42.33 44.76 37.22"/>
<path d="m37.49,27.94c-.98-1.25-2.87-1.25-3.86,0l-3.28,4.18h10.4l-3.26-4.17Z"/>
<path d="m98.68,69.04l-32.16-41.1c-.98-1.25-2.87-1.25-3.86,0l-12.65,16.01-2.77,3.5h-28.95l-4.02,5.11h29.03l-4.02,5.11H10.26l-4.02,5.11h29.03l-4.02,5.11H2.22l-.9,1.15c-1.26,1.61-.12,3.96,1.93,3.96h93.5c2.04,0,3.19-2.35,1.93-3.96Z"/>
</svg>

After

Width:  |  Height:  |  Size: 539 B

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<defs>
<style>
.cls-1 {
fill: #fff;
}
</style>
</defs>
<polygon class="cls-1" points="44.76 37.22 26.34 37.22 22.32 42.33 48.75 42.33 44.76 37.22"/>
<path class="cls-1" d="m37.49,27.94c-.98-1.25-2.87-1.25-3.86,0l-3.28,4.18h10.4l-3.26-4.17Z"/>
<path class="cls-1" d="m98.68,69.04l-32.16-41.1c-.98-1.25-2.87-1.25-3.86,0l-12.65,16.01-2.77,3.5h-28.95l-4.02,5.11h29.03l-4.02,5.11H10.26l-4.02,5.11h29.03l-4.02,5.11H2.22l-.9,1.15c-1.26,1.61-.12,3.96,1.93,3.96h93.5c2.04,0,3.19-2.35,1.93-3.96Z"/>
</svg>

After

Width:  |  Height:  |  Size: 668 B

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 450">
<g>
<path d="m363.5,253.29h-13.56s0-45.66,0-45.66l-17.49,31.46h-9.11l-17.8-31.77v45.99h-13.56v-71.86h13.56l22.36,40.88,22.05-40.88h13.56v71.85Z"/>
<path d="m372.51,229.05v-6.42c0-8,2.17-14.09,6.52-18.27,4.35-4.17,10.35-6.26,18.01-6.26s13.54,2.1,17.85,6.31,6.47,10.28,6.47,18.22v6.42c0,8.07-2.16,14.21-6.47,18.42-4.31,4.21-10.26,6.31-17.85,6.31s-13.66-2.1-18.01-6.31c-4.35-4.21-6.52-10.35-6.52-18.42Zm36.12-6.21c0-4.55-1-7.92-3-10.09-2-2.17-4.86-3.26-8.59-3.26-7.87,0-11.8,4.45-11.8,13.35v6.11c0,8.9,3.93,13.35,11.8,13.35s11.59-4.45,11.59-13.35v-6.11Z"/>
<path d="m445.89,235.47c0,2.35.5,4.02,1.5,5.02,1,1,2.67,1.5,5.02,1.5h4.55v10.87h-6.93c-5.45,0-9.63-1.43-12.52-4.29-2.9-2.86-4.35-6.95-4.35-12.27v-28.25h-9.42v-9h9.42v-10.56l12.73-4.45v15.01h12.21v9h-12.21v27.43Z"/>
<path d="m479.63,199.04v6.11c4-4.69,9.11-7.04,15.32-7.04s11.52,2.07,15.32,6.21,5.69,10.11,5.69,17.91v6.73c0,5.18-.91,9.64-2.74,13.4-1.83,3.76-4.33,6.61-7.5,8.54-3.17,1.93-6.8,2.9-10.87,2.9-6.42,0-11.38-2.14-14.9-6.42v21.63h-12.83v-69.97h12.52Zm23.49,29.39v-5.59c0-8.9-3.83-13.35-11.49-13.35-3.66,0-6.52,1.16-8.59,3.47-2.07,2.31-3.1,5.54-3.1,9.68v6.83c0,3.93,1.07,7.06,3.21,9.37,2.14,2.31,5.04,3.47,8.69,3.47s6.45-1.16,8.38-3.47c1.93-2.31,2.9-5.78,2.9-10.4Z"/>
<path d="m537.48,181.44v23.29c1.86-2.07,4.14-3.64,6.83-4.71,2.69-1.07,5.59-1.6,8.69-1.6,6.21,0,11.04,1.83,14.49,5.49s5.17,8.8,5.17,15.42v33.53h-12.83v-30.95c0-8.07-3.55-12.11-10.66-12.11-3.66,0-6.52,1.05-8.59,3.16-2.07,2.1-3.11,5.26-3.11,9.47v30.43h-12.83v-71.42h12.83Z"/>
<path d="m606.52,257.52c-1.52,3.93-3.76,6.8-6.73,8.59-2.97,1.79-6.83,2.69-11.59,2.69h-4.97v-11.49h5.59c2.48,0,4.29-.33,5.43-.98,1.14-.66,2.05-1.88,2.74-3.67l.31-.93-21.94-52.68h13.87l13.97,36.02,12.83-36.02h13.35l-22.87,58.48Z"/>
<path d="m631.56,236.3v-.31h12.01v.21c0,2.14,1,3.83,3,5.07s4.97,1.86,8.9,1.86c3.66,0,6.43-.52,8.33-1.55s2.85-2.55,2.85-4.55c0-1.66-.67-2.93-2.02-3.83-1.34-.9-3.54-1.52-6.57-1.86l-7.97-.93c-6.07-.76-10.54-2.36-13.4-4.81-2.86-2.45-4.3-5.95-4.3-10.51,0-5.31,2-9.42,6-12.32,4-2.9,9.49-4.35,16.46-4.35s12.8,1.45,16.87,4.35c4.07,2.9,6.11,6.9,6.11,12.01v.41h-11.9v-.31c0-2.07-.93-3.66-2.79-4.76-1.86-1.1-4.66-1.66-8.38-1.66-3.52,0-6.12.5-7.81,1.5-1.69,1-2.54,2.43-2.54,4.3,0,1.52.59,2.73,1.76,3.62,1.17.9,3.14,1.48,5.9,1.76l8.59,1.04c6.21.69,10.8,2.33,13.77,4.92,2.97,2.59,4.45,6.12,4.45,10.61,0,5.59-2.16,9.87-6.47,12.83-4.31,2.97-10.19,4.45-17.65,4.45s-13.01-1.5-17.08-4.5c-4.07-3-6.11-7.23-6.11-12.68Z"/>
</g>
<g>
<polygon points="189.56 197.29 160.73 197.29 154.44 205.29 195.82 205.29 189.56 197.29"/>
<path d="m178.19,182.76c-1.53-1.96-4.5-1.96-6.04,0l-5.14,6.54h16.28l-5.11-6.53Z"/>
<path d="m273.97,247.09l-50.33-64.33c-1.53-1.96-4.5-1.96-6.04,0l-19.8,25.06-4.34,5.47h-45.31l-6.29,8h45.44l-6.29,8h-45.43l-6.29,8h45.44l-6.29,8h-45.44l-1.41,1.79c-1.98,2.52-.19,6.21,3.01,6.21h146.35c3.2,0,4.99-3.68,3.02-6.2Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 450">
<defs>
<style>
.cls-1 {
fill: #fff;
}
</style>
</defs>
<g>
<path class="cls-1" d="m363.5,253.29h-13.56s0-45.66,0-45.66l-17.49,31.46h-9.11l-17.8-31.77v45.99h-13.56v-71.86h13.56l22.36,40.88,22.05-40.88h13.56v71.85Z"/>
<path class="cls-1" d="m372.51,229.05v-6.42c0-8,2.17-14.09,6.52-18.27,4.35-4.17,10.35-6.26,18.01-6.26s13.54,2.1,17.85,6.31,6.47,10.28,6.47,18.22v6.42c0,8.07-2.16,14.21-6.47,18.42-4.31,4.21-10.26,6.31-17.85,6.31s-13.66-2.1-18.01-6.31c-4.35-4.21-6.52-10.35-6.52-18.42Zm36.12-6.21c0-4.55-1-7.92-3-10.09-2-2.17-4.86-3.26-8.59-3.26-7.87,0-11.8,4.45-11.8,13.35v6.11c0,8.9,3.93,13.35,11.8,13.35s11.59-4.45,11.59-13.35v-6.11Z"/>
<path class="cls-1" d="m445.89,235.47c0,2.35.5,4.02,1.5,5.02,1,1,2.67,1.5,5.02,1.5h4.55v10.87h-6.93c-5.45,0-9.63-1.43-12.52-4.29-2.9-2.86-4.35-6.95-4.35-12.27v-28.25h-9.42v-9h9.42v-10.56l12.73-4.45v15.01h12.21v9h-12.21v27.43Z"/>
<path class="cls-1" d="m479.63,199.04v6.11c4-4.69,9.11-7.04,15.32-7.04s11.52,2.07,15.32,6.21,5.69,10.11,5.69,17.91v6.73c0,5.18-.91,9.64-2.74,13.4-1.83,3.76-4.33,6.61-7.5,8.54-3.17,1.93-6.8,2.9-10.87,2.9-6.42,0-11.38-2.14-14.9-6.42v21.63h-12.83v-69.97h12.52Zm23.49,29.39v-5.59c0-8.9-3.83-13.35-11.49-13.35-3.66,0-6.52,1.16-8.59,3.47-2.07,2.31-3.1,5.54-3.1,9.68v6.83c0,3.93,1.07,7.06,3.21,9.37,2.14,2.31,5.04,3.47,8.69,3.47s6.45-1.16,8.38-3.47c1.93-2.31,2.9-5.78,2.9-10.4Z"/>
<path class="cls-1" d="m537.48,181.44v23.29c1.86-2.07,4.14-3.64,6.83-4.71,2.69-1.07,5.59-1.6,8.69-1.6,6.21,0,11.04,1.83,14.49,5.49s5.17,8.8,5.17,15.42v33.53h-12.83v-30.95c0-8.07-3.55-12.11-10.66-12.11-3.66,0-6.52,1.05-8.59,3.16-2.07,2.1-3.11,5.26-3.11,9.47v30.43h-12.83v-71.42h12.83Z"/>
<path class="cls-1" d="m606.52,257.52c-1.52,3.93-3.76,6.8-6.73,8.59-2.97,1.79-6.83,2.69-11.59,2.69h-4.97v-11.49h5.59c2.48,0,4.29-.33,5.43-.98,1.14-.66,2.05-1.88,2.74-3.67l.31-.93-21.94-52.68h13.87l13.97,36.02,12.83-36.02h13.35l-22.87,58.48Z"/>
<path class="cls-1" d="m631.56,236.3v-.31h12.01v.21c0,2.14,1,3.83,3,5.07s4.97,1.86,8.9,1.86c3.66,0,6.43-.52,8.33-1.55s2.85-2.55,2.85-4.55c0-1.66-.67-2.93-2.02-3.83-1.34-.9-3.54-1.52-6.57-1.86l-7.97-.93c-6.07-.76-10.54-2.36-13.4-4.81-2.86-2.45-4.3-5.95-4.3-10.51,0-5.31,2-9.42,6-12.32,4-2.9,9.49-4.35,16.46-4.35s12.8,1.45,16.87,4.35c4.07,2.9,6.11,6.9,6.11,12.01v.41h-11.9v-.31c0-2.07-.93-3.66-2.79-4.76-1.86-1.1-4.66-1.66-8.38-1.66-3.52,0-6.12.5-7.81,1.5-1.69,1-2.54,2.43-2.54,4.3,0,1.52.59,2.73,1.76,3.62,1.17.9,3.14,1.48,5.9,1.76l8.59,1.04c6.21.69,10.8,2.33,13.77,4.92,2.97,2.59,4.45,6.12,4.45,10.61,0,5.59-2.16,9.87-6.47,12.83-4.31,2.97-10.19,4.45-17.65,4.45s-13.01-1.5-17.08-4.5c-4.07-3-6.11-7.23-6.11-12.68Z"/>
</g>
<g>
<polygon class="cls-1" points="189.56 197.29 160.73 197.29 154.44 205.29 195.82 205.29 189.56 197.29"/>
<path class="cls-1" d="m178.19,182.76c-1.53-1.96-4.5-1.96-6.04,0l-5.14,6.54h16.28l-5.11-6.53Z"/>
<path class="cls-1" d="m273.97,247.09l-50.33-64.33c-1.53-1.96-4.5-1.96-6.04,0l-19.8,25.06-4.34,5.47h-45.31l-6.29,8h45.44l-6.29,8h-45.43l-6.29,8h45.44l-6.29,8h-45.44l-1.41,1.79c-1.98,2.52-.19,6.21,3.01,6.21h146.35c3.2,0,4.99-3.68,3.02-6.2Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -0,0 +1,16 @@
/* Custom CSS for MotrixSim Documentation */
/* Logo adjustments for PyData Sphinx Theme */
.navbar-brand img {
max-height: 130px !important;
height: 130px !important;
width: auto !important;
}
.bd-main .bd-content .bd-article-container {
max-width: 80em; /* default is 60em */
}
.bd-page-width {
max-width: 100rem; /* default is 88rem */
}

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:10b78648c8a4ded8c56292377d5a0823a1c8e9cc5f8764fa8082f6a3341a41ad
size 176434

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:987c2ac4e86e1fe6634a16b3ea5fb1a633f07b41d39918c6a23df4da88d5b45c
size 140437

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:194d9a0d40b04d9d41aff936503a1fc8a4d95e1bb3ba28e968996cc29793060a
size 161837

View File

@@ -0,0 +1,14 @@
function openVideoControls(video) {
video.controls = true;
}
function isWeChatMobile() {
const ua = navigator.userAgent.toLowerCase();
return /micromessenger/.test(ua) && /android|iphone|ipad|ipod/.test(ua);
}
if (isWeChatMobile()) {
document.addEventListener("DOMContentLoaded", function () {
document.querySelectorAll("video").forEach(openVideoControls);
});
}

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:723e272be6aff6e0c10f72f871b5674c509a3fa9b6b0ca26c0671b4b67d343ee
size 1816175

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e877e592a3fcf323ca37aeced5e0d5383a128d82958dc3044816020c64e25d01
size 1628617

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:540a9fa3591f456c509f8c3383dc2dbe2106cdf75a7415d49307d74a9388105f
size 4770163

183
docs/source/conf.py Normal file
View File

@@ -0,0 +1,183 @@
# Copyright (C) 2020-2025 Motphys Technology Co., Ltd. All Rights Reserved.
#
# 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.
# ==============================================================================
# Configuration file for the Sphinx documentation builder.
#
# For the full list of built-in configuration values, see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
import os
import shutil
from pathlib import Path
__version__ = "0.1.0"
# -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
project = "Motrixlab"
copyright = "2025, Motphys"
author = "Motphys"
release = __version__
version = __version__
language = "zh_CN"
def setup(app):
app.connect("builder-inited", copy_file_by_language)
def copy_file_by_language(app):
"""根据语言配置复制语言目录"""
lang = app.config.language
srcdir = Path(app.srcdir)
lang_dir = srcdir / lang
if lang_dir.exists() and lang_dir.is_dir():
shutil.copytree(lang_dir, srcdir, dirs_exist_ok=True, copy_function=shutil.copy2)
print(f"Copied language directory: {lang_dir} -> {srcdir}")
else:
print(f"[WARNING] Language directory not found: {lang_dir}")
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
extensions = [
# Include documentation from docstrings
"sphinx.ext.autodoc",
# Support for NumPy and Google style docstrings
"sphinx.ext.napoleon",
# Generate autodoc summaries
"sphinx.ext.autosummary",
# Extending your autodoc API docs with a summary
"autodocsumm",
"sphinx.ext.mathjax",
"sphinx.ext.viewcode",
"sphinx.ext.intersphinx",
"sphinx_copybutton",
"myst_parser",
"sphinx_subfigure",
"sphinxcontrib.video",
"sphinx_togglebutton",
"sphinx_design",
]
# https://sphinx-design.readthedocs.io/en/pydata-theme/get_started.html#usage
myst_enable_extensions = ["colon_fence", "deflist"]
### Autodoc configurations ###
# put type hints inside the signature instead of the description (easier to maintain)
autodoc_typehints = "both"
# Define the order in which automodule and autoclass members are listed
autodoc_member_order = "groupwise"
# default autodoc settings
autodoc_default_options = {
"autosummary": True,
}
autodoc_typehints_description_target = "all"
autodoc_default_flags = ["members", "show-inheritance", "undoc-members"]
# generate autosummary even if no references
autosummary_generate = True
autosummary_generate_overwrite = False
# Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"]
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = ["index_*.md", "api_reference/low**", "zh_CN/**", "en/**"]
# https://myst-parser.readthedocs.io/en/latest/syntax/optional.html
myst_enable_extensions = ["colon_fence", "dollarmath"]
# https://github.com/executablebooks/MyST-Parser/issues/519#issuecomment-1037239655
myst_heading_anchors = 4
# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
html_title = "Motrixlab Documentation"
html_theme = "pydata_sphinx_theme"
html_static_path = ["_static"]
html_logo = "_static/Motphys_logo_Black.svg"
html_favicon = "_static/Motphys_Logo_only_Black_100x100px.svg"
html_show_copyright = True
html_show_sphinx = False
html_last_updated_fmt = "" # to reveal the build date in the pages meta
html_theme_options = {
# https://pydata-sphinx-theme.readthedocs.io/en/stable/user_guide/navigation.html
"show_nav_level": 2,
# https://pydata-sphinx-theme.readthedocs.io/en/stable/user_guide/source-buttons.html#add-an-edit-button
"use_edit_page_button": False,
# https://pydata-sphinx-theme.readthedocs.io/en/stable/user_guide/indices.html#add-indices-links
"primary_sidebar_end": ["indices.html"],
"logo": {
"image_light": "_static/Motphys_logo_Black.svg",
"image_dark": "_static/Motphys_logo_White.svg",
},
# https://pydata-sphinx-theme.readthedocs.io/en/stable/user_guide/layout.html#configure-the-navbar-center-alignment
"navbar_start": ["navbar-logo"],
"navbar_center": ["navbar-nav"], # "version-switcher"
"navbar_persistent": ["search-field.html"],
"navbar_end": ["navbar-icon-links", "theme-switcher"],
# https://pydata-sphinx-theme.readthedocs.io/en/stable/user_guide/layout.html#configure-the-navbar-center-alignment
"navbar_align": "content",
# https://pydata-sphinx-theme.readthedocs.io/en/stable/user_guide/page-toc.html#per-page-secondary-sidebar-content
"secondary_sidebar_items": ["page-toc", "sidebar-ethical-ads"],
# https://pydata-sphinx-theme.readthedocs.io/en/stable/user_guide/layout.html#footer-content
"show_prev_next": True,
# https://pydata-sphinx-theme.readthedocs.io/en/stable/user_guide/announcements.html#version-warning-banners
"show_version_warning_banner": False,
# https://pydata-sphinx-theme.readthedocs.io/en/stable/user_guide/layout.html#footer-content
"footer_start": ["copyright", "sphinx-version"],
"footer_end": ["theme-version"],
# https://pydata-sphinx-theme.readthedocs.io/en/stable/user_guide/header-links.html#navigation-bar-external-links
"external_links": [
{"name": "Issues", "url": "https://github.com/Motphys/MotrixLab/issues"},
{"name": "Discussions", "url": "https://github.com/Motphys/MotrixLab/discussions"},
],
"header_links_before_dropdown": 2,
# https://pydata-sphinx-theme.readthedocs.io/en/stable/user_guide/header-links.html#icon-links
"icon_links": [
{
# Label for this link
"name": "GitHub",
# URL where the link will redirect
"url": "https://github.com/Motphys/MotrixLab", # required
# Icon class (if "type": "fontawesome"), or path to local image (if "type": "local")
"icon": "fa-brands fa-github",
"type": "fontawesome",
},
{
"name": "About Motphys",
"url": "https://www.motphys.com",
"icon": "fa-solid fa-building",
"type": "fontawesome",
},
],
}
project_version = os.environ.get("READTHEDOCS_VERSION")
if project_version is None:
project_version = __version__
html_context = {}
html_css_files = [
"css/custom.css",
]
html_js_files = [
"js/custom.js",
]

66
docs/source/en/index.md Normal file
View File

@@ -0,0 +1,66 @@
# MotrixLab
MotrixLab is a general-purpose machine learning architecture designed for robot training. Built on the MotrixSim simulation platform, it enables policy training on both CPU and GPU, providing flexible and easy-to-use training capabilities for users across various operating systems and hardware devices.
::::{grid} 1 2 3 3
:gutter: 2 2 2 2
:::{grid-item-card}
```{video} _static/videos/cartpole.mp4
:poster: _static/images/poster/cartpole.jpg
:nocontrols:
:autoplay:
:playsinline:
:muted:
:loop:
:width: 100%
```
:::
:::{grid-item-card}
```{video} _static/videos/go1_walk.mp4
:poster: _static/images/poster/go1_walk.jpg
:nocontrols:
:autoplay:
:playsinline:
:muted:
:loop:
:width: 100%
```
:::
:::{grid-item-card}
```{video} _static/videos/dm_walker.mp4
:poster: _static/images/poster/dm_walker.jpg
:nocontrols:
:autoplay:
:playsinline:
:muted:
:loop:
:width: 100%
```
:::
::::
## Key Features
- **Cross-Platform Software Support**: Compatible with Windows and Linux operating systems
- **CPU Simulation Support**: Supports CPU-based simulation, significantly reducing GPU requirements
## Use Cases
- Robot control algorithm development and testing
- Building reinforcement learning environments
- Education and research
```{toctree}
:maxdepth: 1
user_guide/index
```

View File

@@ -0,0 +1,112 @@
# CartPole Training Example
CartPole is a classic control task in reinforcement learning. The goal is to keep the pole balanced by controlling the cart's left-right movement.
![cartpole](/_static/images/poster/cartpole.jpg)
## Task Description
- **State Space**: Cart position, cart velocity, pole angle, pole angular velocity
- **Action Space**: Apply force left or right
- **Reward Function**: +1 reward for each step the pole stays upright
- **Termination Conditions**: Pole angle exceeds ±15 degrees or episode length exceeds 10 seconds
## Quick Start
### 1. Environment Preview
```bash
uv run scripts/view.py --env cartpole
```
### 2. Start Training
```bash
# Train with default parameters
uv run scripts/train.py --env cartpole
# Customize number of environments
uv run scripts/train.py --env cartpole --num-envs 1024
# Enable rendering (visualize during training)
uv run scripts/train.py --env cartpole --render
```
### 3. View Training Progress
```bash
uv run tensorboard --logdir runs/cartpole
```
### 4. Test Training Results
```bash
# Automatically find best policy for testing (recommended)
uv run scripts/play.py --env cartpole
# Manually specify policy file for testing
uv run scripts/play.py --env cartpole --policy runs/cartpole/nn/best_policy.pickle
```
> **Tip**: The system will automatically find the latest and best policy files in the `runs/cartpole/` directory for testing. You can also manually specify specific policy files using the `--policy` parameter.
## Configuration Parameters
Main configuration parameters for the CartPole environment:
```python
@dataclass
class CartPoleEnvCfg(EnvCfg):
model_file: str = "path/to/inverted_pendulum.xml" # MJCF model file
reset_noise_scale: float = 0.01 # Reset noise
max_episode_seconds: float = 10.0 # Maximum episode length
```
Training configuration parameters:
```python
from dataclasses import dataclass
from motrix_rl.skrl.cfg import PPOCfg
from motrix_rl import registry
@registry.rlcfg("cartpole")
@dataclass
class CartPolePPO(PPOCfg):
max_env_steps: int = 10_000_000 # Maximum environment steps
check_point_interval: int = 500 # Checkpoint interval
# Network structure (small network suitable for simple tasks)
policy_hidden_layer_sizes: tuple[int, ...] = (32, 32)
value_hidden_layer_sizes: tuple[int, ...] = (32, 32)
# PPO parameters
rollouts: int = 32 # Experience replay rounds
learning_epochs: int = 5 # Training rounds
mini_batches: int = 4 # Number of mini-batches
```
**Note**: CartPole is a simple task and currently uses universal configuration. If you need to create specialized configurations for different training backends (JAX/Torch), refer to the environment configuration documentation examples.
## Custom Training
You can override default configurations through command line arguments:
```bash
uv run scripts/train.py --env cartpole \
--num-envs 1024 \
--train-backend jax \
--sim-backend np
```
## Expected Results
- Pole angle stays within ±5 degrees most of the time
- Cart displacement range is reasonable
## Troubleshooting
If training performance is poor, you can try:
1. Adjust learning rate (try 1e-4 to 1e-3)
2. Increase number of environments (more parallel training)
3. Adjust reward function weights
4. Check if physical parameters are reasonable

View File

@@ -0,0 +1,153 @@
# 2D Walker Robot Training Example
The 2D Walker Robot (Walker2D) is a classic robot control task from DeepMind Control Suite. The goal is to achieve standing, walking, and running by controlling the robot's joints.
```{video} /_static/videos/dm_walker.mp4
:poster: _static/images/poster/dm_walker.jpg
:nocontrols:
:autoplay:
:playsinline:
:muted:
:loop:
:width: 100%
```
## Task Description
Walker2D is a 2D planar bipedal robot with multiple joints and actuators:
- **State Space**: Includes rotation angles and angular velocities of various robot parts, torso height and velocity, etc.
- **Action Space**: Control torques for each joint
- **Reward Function**: Mainly composed of maintaining standing balance and forward speed
- **Termination Conditions**: Robot falls or joints reach limit positions
### Three Task Modes
1. **dm-stander**: Static standing task (move_speed = 0.0)
```bash
uv run scripts/train.py --env dm-stander
```
2. **dm-walker**: Walking task (move_speed = 1.0)
```bash
uv run scripts/train.py --env dm-walker
```
3. **dm-runner**: Running task (move_speed = 5.0)
```bash
uv run scripts/train.py --env dm-runner
```
## Quick Start
### 1. Environment Preview
```bash
# View standing task
uv run scripts/view.py --env dm-stander
# View walking task
uv run scripts/view.py --env dm-walker
# View running task
uv run scripts/view.py --env dm-runner
```
### 2. Start Training
```bash
# Train standing task
uv run scripts/train.py --env dm-stander
# Train walking task (default)
uv run scripts/train.py --env dm-walker
# Train running task
uv run scripts/train.py --env dm-runner
# Customize number of environments
uv run scripts/train.py --env dm-walker --num-envs 512
# Enable rendering (visualize during training)
uv run scripts/train.py --env dm-walker --render
```
### 3. View Training Progress
```bash
uv run tensorboard --logdir runs/dm-walker
```
### 4. Test Training Results
```bash
# Automatically find best policy for testing (recommended)
uv run scripts/play.py --env dm-walker
# Manually specify policy file for testing
uv run scripts/play.py --env dm-walker --policy runs/dm-walker/nn/best_policy.pickle
```
> **Tip**: The system will automatically find the latest and best policy files in the `runs/dm-walker/` directory for testing. Supports dm-stander, dm-walker, dm-runner three task modes.
## Configuration Parameters
### Environment Configuration
```python
@dataclass
class WalkerEnvCfg(EnvCfg):
model_file: str = "walker.xml" # MJCF model file
max_episode_seconds: float = 25.0 # Maximum episode length
sim_dt: float = 0.0125 # Simulation time step
ctrl_dt: float = 0.025 # Control time step
move_speed: float = 1.0 # Target movement speed
stand_height: float = 1.2 # Target standing height
```
### Training Configuration
```python
@dataclass
class WalkerRLCfg(BaseRLCfg):
num_envs: int = 512 # Number of parallel environments
learning_rate: float = 3e-4 # Learning rate
batch_size: int = 512 # Batch size
max_epochs: int = 1000 # Maximum training epochs
```
## Reward Function Design
Walker2D's reward function consists of the following components:
### Basic Standing Reward
```python
# Height reward: keep torso at target height
# Upright reward: keep torso upright
```
### Movement Reward (walking and running tasks)
```python
# Speed reward: track target speed
# Total reward = standing reward * movement weight
```
## Expected Results
1. **dm-stander**:
- Torso height maintained in 1.0-1.4m range
2. **dm-walker**:
- Actual walking speed close to 1.0 m/s
3. **dm-runner**:
- Running speed reaches 4.0-5.0 m/s

View File

@@ -0,0 +1,169 @@
# Unitree GO1 Robot Walking Training Example
Unitree GO1 is a quadruped robot platform. This example demonstrates how to train GO1 to achieve stable gait walking on flat terrain.
```{video} /_static/videos/go1_walk.mp4
:poster: _static/images/poster/go1_walk.jpg
:nocontrols:
:autoplay:
:playsinline:
:muted:
:loop:
:width: 100%
```
## Task Description
The GO1 quadruped robot has 12 degrees of freedom (3 joints per leg) and needs to learn coordinated gait control through deep reinforcement learning:
- **State Space**: 48-dimensional, including robot linear velocity, angular velocity, posture, joint angles, joint velocities, actions, and commands
- **Action Space**: 12-dimensional, controlling target positions of each joint (converted to torques through PD controller)
- **Reward Function**: Composite reward including speed tracking, posture stability, energy efficiency, and other components
- **Termination Conditions**: Robot trunk contacts ground or other unstable states
### Training Task
```bash
uv run scripts/train.py --env go1-flat-terrain-walk
```
## Configuration Parameters
### Environment Configuration
```python
@dataclass
class Go1WalkNpEnvCfg(EnvCfg):
max_episode_seconds: float = 20.0 # Maximum episode length
model_file: str = "scene_motor_actuator.xml"
sim_dt: float = 0.01 # Simulation time step
ctrl_dt: float = 0.01 # Control time step
```
### Training Configuration
```python
from dataclasses import dataclass
from motrix_rl.skrl.cfg import PPOCfg
from motrix_rl import registry
@registry.rlcfg("go1-flat-terrain-walk")
@dataclass
class Go1WalkPPO(PPOCfg):
"""
GO1 quadruped robot walking training configuration
"""
seed = 42
max_env_steps: int = 40960000 # Maximum training steps
num_envs: int = 2048 # Number of parallel environments
# Large network structure (suitable for complex robot control tasks)
policy_hidden_layer_sizes: tuple[int, ...] = (512, 256, 128)
value_hidden_layer_sizes: tuple[int, ...] = (512, 256, 128)
# PPO parameters (optimized for robot tasks)
learning_epochs: int = 2 # Training rounds
mini_batches: int = 32 # Number of mini-batches
learning_rate: float = 1e-3 # Learning rate
```
**Note**: GO1 is a complex task that uses large network structures. If you need to create specialized configurations for different training backends (JAX/Torch), refer to the environment configuration documentation examples.
### Control Configuration
```python
@dataclass
class ControlConfig:
stiffness = 80 # PD controller stiffness [N*m/rad]
damping = 1 # PD controller damping [N*m*s/rad]
action_scale = 0.1 # Action scaling factor
```
### Initial Joint Angles
```python
default_joint_angles = {
"FL_hip": 0.0, # Front left hip joint
"RL_hip": 0.0, # Rear left hip joint
"FR_hip": -0.0, # Front right hip joint
"RR_hip": -0.0, # Rear right hip joint
"FL_thigh": 0.9, # Front left thigh
"RL_thigh": 0.9, # Rear left thigh
"FR_thigh": 0.9, # Front right thigh
"RR_thigh": 0.9, # Rear right thigh
"FL_calf": -1.8, # Front left calf
"RL_calf": -1.8, # Rear left calf
"FR_calf": -1.8, # Front right calf
"RR_calf": -1.8, # Rear right calf
}
```
## Reward Function Design
GO1's reward function is a complex composite function containing multiple components:
### Main Reward Components
```python
reward_config.scales = {
"tracking_lin_vel": 1.0, # Linear velocity tracking reward
"tracking_ang_vel": 0.5, # Angular velocity tracking reward
"feet_air_time": 1.0, # Foot air time reward
"lin_vel_z": -2.0, # Z-axis linear velocity penalty
"ang_vel_xy": -0.05, # XY-axis angular velocity penalty
"orientation": -0.0, # Posture deviation penalty
"torques": -0.00001, # Torque consumption penalty
"dof_acc": -2.5e-7, # Joint acceleration penalty
"action_rate": -0.001, # Action change rate penalty
"hip_pos": -1, # Hip joint position penalty
"calf_pos": -0.3, # Calf joint position penalty
}
```
### Key Reward Functions
#### Velocity Tracking Reward
```python
# Track linear velocity commands (xy plane)
def _reward_tracking_lin_vel(self, data, commands):
# Track angular velocity commands (yaw)
def _reward_tracking_ang_vel(self, data, commands):
```
#### Foot Air Time Reward
```python
def _reward_feet_air_time(self, commands, info):
```
## Observation Space Composition
GO1's observation space is 48-dimensional, containing the following information:
```python
obs = np.hstack([
noisy_linvel, # 3D: Local coordinate system linear velocity
noisy_gyro, # 3D: Gyroscope data
local_gravity, # 3D: Local gravity direction
noisy_joint_angle, # 12D: Joint angles (relative to default values)
noisy_joint_vel, # 12D: Joint velocities
last_actions, # 12D: Previous frame actions
command, # 3D: Velocity commands [vx, vy, vyaw]
])
```
## Motion Velocity Command Generation
Random velocity commands are generated during training to ensure the agent can track different movement speeds:
```python
def resample_commands(self, num_envs: int):
```
## Expected Training Results
1. Stable quadruped gait
2. Good speed tracking

View File

@@ -0,0 +1,84 @@
# Quick Start: Hello MotrixLab
This tutorial demonstrates the MotrixLab workflow through a simple example - loading and training a cartpole environment:
## Environment Preview
We provide a simple script to visualize an environment without executing any training. This helps you verify that system dependencies are correctly configured:
```bash
uv run scripts/view.py --env cartpole
```
This will open a visualization window showing the cartpole physics simulation environment with random actions for demonstration.
## Train Model
Start training the cartpole balancing task:
```bash
uv run scripts/train.py --env cartpole
```
The training process will automatically:
1. Automatically select training backend (JAX or PyTorch) based on hardware environment
2. Create training environments
3. Start PPO algorithm training
Training results will be saved in the `runs/cartpole/` directory, including:
- Training checkpoints
- TensorBoard log files
## Visualize Training Process
If you want to observe the model's learning process during training, you can enable visualization rendering:
```bash
uv run scripts/train.py --env cartpole --render
```
### 🎮 Interactive Rendering Control
> **Important Note**: Visualization significantly reduces training speed and is recommended mainly for debugging and demonstration purposes.
During visualized training, you can use the **spacebar** to dynamically control rendering:
- **Enable Rendering**: Press spacebar to enable visualization and observe robot behavior
- **Disable Rendering**: Press spacebar again to disable rendering and improve training speed
- **Switch Anytime**: No need to restart the program; you can switch at any time during training
This interactive control allows you to observe training effects when needed and enjoy fast training when not needed. This feature also works during inference.
## View Training Results
Use TensorBoard to view training progress:
```bash
uv run tensorboard --logdir runs/cartpole
```
## Test Trained Model
After training is complete, test the trained policy:
```bash
# Automatically find best policy for testing (recommended)
uv run scripts/play.py --env cartpole
# Manually specify policy file for testing (if you need a specific version)
uv run scripts/play.py --env cartpole --policy runs/cartpole/YOUR_RESULT_NUMBER/best_agent.pickle
```
> **Tip**: The system will automatically find the latest and best policy files in the `runs/cartpole/` directory. Usually, using the auto-discovery feature is sufficient.
## That Completes Our Example
Next, you can try modifying parameters to observe physical effects under different settings, or try other environments.
## Next Steps
- Learn about the [Basic Framework](../tutorial/basic_frame.md)
- Study [Physics Environment Configuration](../tutorial/physics_environment.md)
- View more [Training Examples](../demo/cartpole.md)

View File

@@ -0,0 +1,55 @@
# Installation Environment
## Installation Requirements
- **Python Version**: {bdg-danger-line}`3.10.*`
| Python Version | Support Status |
| :------------: | :------------: |
| ≤ 3.9 | ❌ |
| 3.10 | ✅ |
| ≥ 3.11 | ❌ |
- **Package Manager**: {bdg-danger-line}`UV`
- **System and Architecture**:
- {bdg-danger-line}`Windows(x86_64)`
- {bdg-danger-line}`Linux(x86_64)`
```{note}
Features supported on each platform:
| Operating System | CPU Simulation | Interactive Viewer | GPU Simulation |
| :--------------: | :------------: | :----------------: | :------------: |
| Linux | ✅ | ✅ | 🛠️ In Development |
| Windows | ✅ | ✅ | 🛠️ In Development |
```
## Installation Method
### Clone Project
```bash
git clone https://github.com/Motphys/MotrixLab.git
cd MotrixLab
```
### Install Dependencies
Use UV to install project dependencies:
```bash
# Install all dependencies
uv sync --all-packages --all-extras
```
If you only need to install one training backend, you can choose to install a specific backend type:
```bash
# Install SKRL JAX (support Linux only)
uv sync --all-packages --extra skrl-jax
# Install SKRL PyTorch
uv sync --all-packages --extra skrl-torch
```

View File

@@ -0,0 +1,32 @@
# User Guide
```{toctree}
:caption: Getting Started
:maxdepth: 1
getting_started/installation
getting_started/hello_motrixlab
```
```{toctree}
:caption: Tutorials
:maxdepth: 1
tutorial/basic_frame
tutorial/physics_environment
tutorial/training_environment_config
tutorial/rewards
tutorial/training_and_result
```
```{toctree}
:caption: Training Examples
:maxdepth: 1
demo/cartpole
demo/dm_walker
demo/locomotion_unitree_go1
```

View File

@@ -0,0 +1,149 @@
# Basic Framework
MotrixLab is a robot reinforcement learning platform. This section introduces MotrixLab's framework design and the relationships between various components. If you are already familiar with reinforcement learning content, you can skip directly to the next section to learn how to develop your own training environments.
## MotrixLab's Framework Design
MotrixLab adopts a layered architecture design, clearly separating training environments from training logic:
```
MotrixLab/
├── motrix_envs/ # Environment layer: Physics simulation and task definition
│ ├── basic/ # Basic environments (cartpole, walker, etc.)
│ ├── locomotion/ # Locomotion environments (GO1 robot, etc.)
│ ├── np/ # NumPy simulation backend framework
│ ├── base.py # Environment base class
│ └── registry.py # Environment registry system
├── motrix_rl/ # Training layer: RL algorithms and configuration
│ ├── skrl/ # SKRL framework integration (JAX/PyTorch)
│ ├── base.py # RL configuration base class
│ └── registry.py # RL configuration registry system
└── scripts
├── train.py # Training entry script
├── play.py # Testing entry script
└── view.py # Visualization script
```
## Core Component Architecture
```
┌─────────────────────────────────────────────────────────────────┐
│ User Interface Layer │
│ train.py │ play.py │ view.py │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Training Algorithm Layer (SKRL) │
│ PPO Trainer │ Network Architecture │ Optimizer │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Environment Implementation Layer │
│ Environment Config(EnvCfg) │ Environment Impl(Env) │ Reward │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Physics Simulation Layer (MotrixSim) │
│ MJCF Model │ Physics Engine │ Collision │
└─────────────────────────────────────────────────────────────────┘
```
## Detailed Core Components
### 1. Training Environment
**Location**: Environment Implementation Layer
The training environment is the core component of MotrixLab, containing three key parts:
- **Environment Configuration (EnvCfg)**: Defines physics simulation parameters (model files, time steps, episode length, etc.) and task-specific parameters
- **Environment Implementation (Env)**: Inherits from base environment class, implements specific task logic, physics simulation interaction, and termination condition checking
- **Reward Function (Reward)**: Implemented in the environment's step method, calculates reward values based on current state and actions
Environments are registered to the system through decorators.
### 2. Reward Function
**Location**: Configuration Management Layer + Environment Implementation Layer
The reward function in MotrixLab adopts a dual-structure design:
- **Configuration Level**: Define reward weights, reward component types, and scaling parameters in configuration classes
- **Implementation Level**: Calculate specific reward values in the environment's `_compute_reward` method based on configuration parameters
This design allows reward functions to be flexibly adjusted through configuration files while implementing complex computational logic in code.
### 3. Configuration Parameters
**Location**: Configuration Management Layer
Configuration parameters adopt a hierarchical management structure:
- **Environment Configuration (EnvCfg)**: Controls physics simulation and task behavior, including simulation parameters, reset noise, time limits, etc.
- **Training Configuration (RLCfg)**: Controls reinforcement learning algorithms, including network structure, learning rate, batch size, training steps, etc.
Configuration classes support inheritance, parameter validation, and runtime overriding, ensuring parameter reasonableness and flexibility.
### 4. Registry System
**Location**: Hub connecting various components
The registry system implements automatic component registration through the decorator pattern:
- Environment configuration classes are registered through `@registry.envcfg()`
- Environment implementation classes are registered through `@registry.env()`, supporting multiple backends
- RL configuration classes are registered through `@registry.rlcfg()`
The registry system achieves component decoupling, making it simple and fast to add new environments or modify configurations.
## Data Flow and Workflow
### Training Process Overview
```
User Command → Configuration Parsing → Environment Creation → Training Loop → Model Save
train.py --env cartpole
Find Configuration Classes → Create Environment → Start PPO Training → Save Model
```
### Core Workflow
1. **Environment Definition**: Create environment configuration classes and implementation classes in `src/motrix_envs/`
2. **Automatic Registration**: Register components to the system through decorators
3. **Configuration Loading**: When starting from command line, the system automatically finds and loads corresponding configurations
4. **Environment Creation**: Factory pattern creates environment instances, supporting parameter override
5. **Training Execution**: PPO algorithm interacts with the environment, collects data and updates policy
6. **Result Saving**: Periodically save checkpoints and final models
### Role of Configuration Parameters
Configuration parameters play a key connecting role throughout the process:
- **Environment Configuration** determines physics simulation behavior (time steps, model files, noise, etc.)
- **Reward Configuration** affects learning signals (reward weights, calculation methods, etc.)
- **Training Configuration** controls algorithm behavior (network structure, learning rate, batch size, etc.)
## Multi-Backend Support
MotrixLab's layered design naturally supports multiple backends:
- **Simulation Backends**: MotrixSim (CPU)
- **Training Backends**: JAX and PyTorch, supporting GPU acceleration
- **Algorithm Framework**: Mainly integrates SKRL, easy to extend to other algorithms
## Design Advantages
This architecture design brings the following core advantages:
1. **Module Decoupling**: Environment development and training logic are completely separated
2. **Flexible Configuration**: Supports hierarchical configuration and runtime parameter override
3. **Strong Extensibility**: Easily add new components through the registry system
4. **Multi-Backend Compatibility**: Same environment can use different simulation and training backends
5. **Experiment-Friendly**: Configurations can be saved and compared, ensuring experimental reproducibility
Through this framework design, MotrixLab provides a clear, flexible, and easy-to-use development platform for robot reinforcement learning.

View File

@@ -0,0 +1,66 @@
# Physics Environment Configuration
Physics environment configuration defines simulation parameters and model file settings in reinforcement learning training.
MotrixLab uses [MotrixSim](https://motrixsim.readthedocs.io/en/latest/user_guide/index.html) as the physics simulation backend.
## Supported File Formats
- [**MJCF**](https://mujoco.readthedocs.io/en/stable/XMLreference.html) (MuJoCo XML format) - Provides rich physics features and simulation configuration
## Model File Configuration
You need to specify model file paths in environment configuration classes:
```python
@registry.envcfg("my-task")
@dataclass
class MyTaskEnvCfg(EnvCfg):
# Model file path (required)
model_file: str = "my_model.xml"
# Simulation time parameters
sim_dt: float = 0.002 # Simulation time step
ctrl_dt: float = 0.02 # Control update frequency
# Episode parameters
max_episode_seconds: float = 20.0
reset_noise_scale: float = 0.01
```
### Recommended Directory Structure
```
motrix_envs/my_task/
├── __init__.py # Module initialization
├── cfg.py # Environment configuration
├── my_model.xml # Physics model file
└── my_env.py # Environment implementation
```
For complex models with many referenced files, it's recommended to use folder management.
## Common Configuration Issues
### File Path Issues
- When using relative paths, ensure paths are relative to the configuration file location
- Avoid using hardcoded absolute paths
- Check file permissions and accessibility
- Ensure all referenced sub-files exist
### Time Step Settings
- `ctrl_dt` should be an integer multiple of `sim_dt`
- `sim_dt` that is too small will affect simulation performance
- `ctrl_dt` that is too large will affect control precision
- Recommend `sim_dt` between 0.001-0.02 seconds
### Simulation Stability
- Avoid excessively large time steps
- Set contact parameters reasonably to avoid penetration
- Mass and inertia distribution should be reasonable
- Joint limits should match actual conditions
Through proper physics environment configuration, you can create accurate and efficient simulation environments for reinforcement learning training.

View File

@@ -0,0 +1,50 @@
# Reward Function Design
The reward function tells the agent what behaviors are desired and is a core part of reinforcement learning environment design.
## Position of Reward Function in Training Loop
In MotrixLab's NpEnv, reward calculation occurs in the `update_state` phase of the `step` function:
```python
# Execution flow of NpEnv.step()
def step(self, actions: np.ndarray) -> NpEnvState:
# 1. Preparation phase: Clear rewards and state
self._prev_physics_step() # reward = 0.0, terminated = False, truncated = False
# 2. Apply actions
self._state = self.apply_action(actions, self._state)
# 3. Physics simulation
self.physics_step() # Execute physics simulation
# 4. Update state ← Reward function is calculated here
self._state = self.update_state(self._state) # Calculate rewards and observations
# 5. Post-processing
self._update_truncate() # Check time truncation
self._reset_done_envs() # Reset completed environments
return self._state
```
You need to implement reward calculation logic in the `update_state` method of subclasses. For specific reward function design ideas, please refer to the training examples.
### Reward Component Design Principles
1. **Separation of Concerns**: Each reward function should handle a specific goal
2. **Weight Configuration**: Manage weights of different components through configuration files
3. **Normalization**: Keep reward values within reasonable ranges
4. **Smoothness**: Avoid hard thresholds, use exponential functions for smooth transitions
This approach makes reward functions modular, facilitating debugging and adjustment of individual component weights.
## Design Principles
1. **Clear Goal Orientation**: Reward functions should directly reflect task goals
2. **Reasonable Reward Range**: Avoid overly large or small reward values to maintain training stability
3. **Balance Exploration and Exploitation**: Appropriately reward behaviors close to goals, avoiding sparse rewards
4. **Avoid Reward Hacking**: Check if agents can obtain high rewards through unintended means
5. **Debug-Friendly**: Output reward decomposition information during development for optimization
By correctly implementing reward calculation in the `update_state` method, you can design effective learning signals for various robot tasks.

View File

@@ -0,0 +1,92 @@
# Training Execution and Result Analysis
This section introduces how to execute reinforcement learning training and how to analyze and use training results.
## Start Training
### Basic Training Commands
```bash
# Train with default parameters
uv run scripts/train.py --env cartpole
# Specify simulation backend
uv run scripts/train.py --env cartpole --sim-backend np
# Specify training backend
uv run scripts/train.py --env cartpole --train-backend jax
uv run scripts/train.py --env cartpole --train-backend torch
```
### Advanced Training Configuration
```bash
# Customize training parameters
uv run scripts/train.py --env cartpole \
--num-envs 1024 \
--train-backend jax \
--sim-backend np
# Enable rendering to monitor training process
uv run scripts/train.py --env cartpole --render
```
### Different Backend Configuration Differences
The system supports configuring different reinforcement learning parameters for different training backends (JAX/Torch). For example:
- **dm-walker environment**:
- JAX backend: `mini_batches: 4`
- Torch backend: `mini_batches: 32`
- **dm-runner environment**:
- JAX backend: `learning_epochs: 4`
- Torch backend: `learning_epochs: 2`
These differences are implemented through the `@rlcfg(env_name, backend="jax/torch")` decorator in configuration classes. The system automatically applies the corresponding configuration based on the selected training backend.
### Supported Command Line Parameters
| Parameter | Description | Default Value |
| ----------------- | ------------------------------- | ------------- |
| `--env` | Environment name | `cartpole` |
| `--sim-backend` | Simulation backend (np) | Auto select |
| `--train-backend` | Training backend (jax/torch) | Auto select |
| `--num-envs` | Number of parallel environments | 2048 |
| `--render` | Enable rendering | False |
> **Note**: Other parameters such as learning rate, network structure, etc., can be set in configuration files. Some environments support configuring different parameters for different training backends.
## Training Process Monitoring
### TensorBoard Monitoring
Start TensorBoard to view training progress:
```bash
uv run tensorboard --logdir runs/{env-name}
```
For example:
```bash
uv run tensorboard --logdir runs/cartpole
```
## Model Evaluation and Testing
### Using Trained Policies
```bash
# Automatically find best policy for testing (recommended)
uv run scripts/play.py --env cartpole
# Manually specify policy file for testing
uv run scripts/play.py --env cartpole --policy runs/cartpole/nn/best_policy.pickle
# Specify number of test environments
uv run scripts/play.py --env cartpole --num-envs 100
```
> **Note**: The system will automatically find the latest and best policy files in the `runs/cartpole/` directory for testing.

View File

@@ -0,0 +1,231 @@
# Training Environment Configuration
MotrixLab provides a flexible configuration system that allows users to customize reinforcement learning training parameters. This section introduces how to configure training environments and reinforcement learning algorithm parameters.
## RL Training Configuration (PPOCfg)
Training configuration defines parameters for reinforcement learning algorithms. MotrixLab now supports configuring different parameters for different training backends.
### Basic Training Configuration
```python
from dataclasses import dataclass
from motrix_rl.skrl.cfg import PPOCfg
from motrix_rl import registry
# Universal configuration (applies to all backends)
@registry.rlcfg("my-task")
@dataclass
class MyTaskRLCfg(PPOCfg):
# Environment parameters
num_envs: int = 2048 # Number of parallel environments during training
play_num_envs: int = 16 # Number of parallel environments during evaluation
# PPO algorithm parameters
learning_rate: float = 3e-4 # Learning rate
rollouts: int = 32 # Experience replay rounds
learning_epochs: int = 10 # Number of epochs per update
mini_batches: int = 32 # Number of mini-batches
discount_factor: float = 0.99 # Discount factor
grad_norm_clip: float = 1.0 # Gradient clipping
# Network structure parameters
policy_hidden_layer_sizes: tuple[int, ...] = (256, 128, 64) # Policy network hidden layers
value_hidden_layer_sizes: tuple[int, ...] = (256, 128, 64) # Value network hidden layers
# Training control parameters
max_env_steps: int = 1_000_000 # Maximum environment steps
check_point_interval: int = 10_000 # Checkpoint interval
```
### Different Backend Configurations
```python
# JAX backend specific configuration
@registry.rlcfg("my-task", backend="jax")
@dataclass
class MyTaskJAXCfg(PPOCfg):
# JAX optimized configuration
learning_rate: float = 2e-4 # JAX backend uses smaller learning rate
mini_batches: int = 4 # JAX supports large batches, fewer mini-batches
learning_epochs: int = 4 # Training rounds
num_envs: int = 2048 # More parallel environments
# Torch backend specific configuration
@registry.rlcfg("my-task", backend="torch")
@dataclass
class MyTaskTorchCfg(PPOCfg):
# Torch optimized configuration
learning_rate: float = 1e-4 # Torch backend uses even smaller learning rate
mini_batches: int = 32 # Torch needs more mini-batches
learning_epochs: int = 2 # Fewer training rounds
num_envs: int = 1024 # Fewer parallel environments
```
### Complete Configuration Example
```python
@dataclass
class CompletePPOConfig(PPOCfg):
"""
Complete reinforcement learning training configuration example
Contains all configuration parameters from basic to advanced
"""
# ===== Basic Training Parameters =====
seed: Optional[int] = None # Random seed
num_envs: int = 2048 # Number of parallel environments during training
play_num_envs: int = 16 # Number of parallel environments during evaluation
max_env_steps: int = 2_048_000 # Maximum training steps
check_point_interval: int = 1000 # Checkpoint save interval
# ===== PPO Algorithm Core Parameters =====
learning_rate: float = 3e-4 # Learning rate
rollouts: int = 32 # Experience replay rounds
learning_epochs: int = 2 # Number of training rounds per update
mini_batches: int = 32 # Number of mini-batches
discount_factor: float = 0.99 # Discount factor
lambda_param: float = 0.95 # GAE parameter
grad_norm_clip: float = 1.0 # Gradient clipping
# ===== PPO Clipping Parameters =====
ratio_clip: float = 0.2 # PPO clipping ratio
value_clip: float = 0.2 # Value clipping
clip_predicted_values: bool = True # Clip predicted values
# ===== Loss Function Parameters =====
entropy_loss_scale: float = 0.0 # Entropy loss coefficient
value_loss_scale: float = 2.0 # Value loss coefficient
kl_threshold: float = 0 # KL divergence threshold
# ===== Learning Rate Scheduler =====
learning_rate_scheduler_kl_threshold: float = 0.008 # Adaptive learning rate KL threshold
# ===== Network Architecture Configuration =====
# Small network (suitable for simple tasks like CartPole)
# policy_hidden_layer_sizes: tuple[int, ...] = (128, 64)
# value_hidden_layer_sizes: tuple[int, ...] = (128, 64)
# Medium network (default configuration, suitable for most tasks)
policy_hidden_layer_sizes: tuple[int, ...] = (256, 128, 64)
value_hidden_layer_sizes: tuple[int, ...] = (256, 128, 64)
# Large network (suitable for complex tasks like robot control)
# policy_hidden_layer_sizes: tuple[int, ...] = (512, 256, 128)
# value_hidden_layer_sizes: tuple[int, ...] = (512, 256, 128)
# ===== Network Sharing Configuration =====
share_policy_value_features: bool = True # Policy and value networks share feature extraction layers
# ===== Training Control Parameters =====
random_timesteps: int = 0 # Random timesteps
learning_starts: int = 0 # Timesteps to start learning
time_limit_bootstrap: bool = True # Time limit bootstrap
# ===== Reward Shaping =====
rewards_shaper_scale: float = 1.0 # Reward scaling factor
```
## Configuration Usage Methods
### 1. Default Configuration Usage
```bash
# Use configuration given in code
uv run scripts/train.py --env my-task
# Specify training backend, system will automatically select corresponding backend configuration
uv run scripts/train.py --env my-task --train-backend jax
uv run scripts/train.py --env my-task --train-backend torch
```
### 2. Command Line Parameter Override
```bash
# Override supported command line parameters
uv run scripts/train.py --env my-task \
--num-envs 1024 \
--train-backend jax \
--sim-backend np
# System will automatically select JAX backend configuration
```
### 3. Configuration Priority
System selects configuration in the following priority:
1. **Backend-specific configuration**: If there is a configuration decorated with `@rlcfg(env_name, backend="jax/torch")`
2. **Universal configuration**: If there is a configuration decorated with `@rlcfg(env_name)` (no backend parameter)
3. **Configuration override**: Command line `cfg_override` parameter
For example:
```python
# Highest priority - backend specific configuration
@rlcfg("my-task", backend="jax")
@dataclass
class MyTaskJAXCfg(PPOCfg):
mini_batches: int = 4
# Second priority - universal configuration
@rlcfg("my-task")
@dataclass
class MyTaskRLCfg(PPOCfg):
mini_batches: int = 32
# When using --train-backend jax, system will select MyTaskJAXCfg
# When using --train-backend torch, system will select MyTaskRLCfg
```
#### User Configurable Parameters
| MotrixLab Configuration Class | SKRL Framework Parameter | Description |
| -------------------------------------- | --------------------------------------------- | ----------------------------------- |
| `learning_rate` | `learning_rate` | Learning rate |
| `rollouts` | `rollouts` | Experience replay rounds |
| `learning_epochs` | `learning_epochs` | Training rounds |
| `mini_batches` | `mini_batches` | Number of mini-batches |
| `discount_factor` | `discount_factor` | Discount factor |
| `grad_norm_clip` | `grad_norm_clip` | Gradient clipping |
| `lambda_param` | `lambda` | GAE parameter |
| `ratio_clip` | `ratio_clip` | PPO clipping ratio |
| `value_clip` | `value_clip` | Value clipping |
| `clip_predicted_values` | `clip_predicted_values` | Clip predicted values |
| `entropy_loss_scale` | `entropy_loss_scale` | Entropy loss coefficient |
| `value_loss_scale` | `value_loss_scale` | Value loss coefficient |
| `kl_threshold` | `kl_threshold` | KL divergence threshold |
| `random_timesteps` | `random_timesteps` | Random timesteps |
| `learning_starts` | `learning_starts` | Learning start timesteps |
| `time_limit_bootstrap` | `time_limit_bootstrap` | Time limit bootstrap |
| `learning_rate_scheduler_kl_threshold` | `learning_rate_scheduler_kwargs.kl_threshold` | Adaptive learning rate KL threshold |
| `check_point_interval` | `experiment.write_interval` | Log write interval |
| `check_point_interval` | `experiment.checkpoint_interval` | Checkpoint save interval |
| `rewards_shaper_scale` | `rewards_shaper` | Reward scaling function |
#### Preprocessor Parameters
| SKRL Framework Parameter | Type | Description |
| ------------------------ | --------------------- | ------------------- |
| `state_preprocessor` | RunningStandardScaler | State normalization |
| `value_preprocessor` | RunningStandardScaler | Value normalization |
### Configuration Hierarchy Summary
```
User Configuration Class (PPOCfg)
↓ Backend specific selection
Backend Configuration (JAX/Torch)
↓ Parameter mapping
SKRL Framework Configuration Dictionary
↓ Pass to
PPO Agent
↓ Execute
Reinforcement Learning Training
```
This design allows users to:
1. Control complex training parameters through simple configuration classes
2. Configure different parameters for different training backends to achieve optimal performance
3. Maintain full compatibility with the SKRL framework

View File

@@ -0,0 +1,66 @@
# MotrixLab
MotrixLab 是一个为机器人训练设计的机器学习通用架构。它基于 MotrixSim 仿真平台搭建,可以实现在 CPU 或 GPU 上的策略训练,为各类操作系统和硬件设备使用者,提供灵活易用的训练能力。
::::{grid} 1 2 3 3
:gutter: 2 2 2 2
:::{grid-item-card}
```{video} _static/videos/cartpole.mp4
:poster: _static/images/poster/cartpole.jpg
:nocontrols:
:autoplay:
:playsinline:
:muted:
:loop:
:width: 100%
```
:::
:::{grid-item-card}
```{video} _static/videos/go1_walk.mp4
:poster: _static/images/poster/go1_walk.jpg
:nocontrols:
:autoplay:
:playsinline:
:muted:
:loop:
:width: 100%
```
:::
:::{grid-item-card}
```{video} _static/videos/dm_walker.mp4
:poster: _static/images/poster/dm_walker.jpg
:nocontrols:
:autoplay:
:playsinline:
:muted:
:loop:
:width: 100%
```
:::
::::
## 主要特性
- **跨软件平台能力**: 支持 Windows 和 Linux 操作系统环境
- **支持 CPU 仿真**: 使用 MotrixSim 在 CPU 上进行物理仿真,对 GPU 的要求大幅降低
## 适用场景
- 机器人控制算法开发和测试
- 强化学习环境构建
- 教育和研究
```{toctree}
:maxdepth: 1
user_guide/index
```

View File

@@ -0,0 +1,99 @@
# 倒立摆训练示例
倒立摆CartPole是强化学习中的经典控制任务目标是通过控制小车左右移动来保持杆子平衡。
![cartpole](/_static/images/poster/cartpole.jpg)
## 任务描述
- **状态空间**:小车位置、小车速度、杆子角度、杆子角速度
- **动作空间**:向左或向右施加力
- **奖励函数**:每一步保持杆子不倒下获得+1 奖励
- **终止条件**:杆子角度超过 ±15 度或 episode 长度超过 10 秒
## 快速开始
### 1. 环境预览
```bash
uv run scripts/view.py --env cartpole
```
### 2. 开始训练
```bash
# 使用默认参数训练
uv run scripts/train.py --env cartpole
# 自定义环境数量
uv run scripts/train.py --env cartpole --num-envs 1024
# 启用渲染(训练时可视化)
uv run scripts/train.py --env cartpole --render
```
### 3. 查看训练进度
```bash
uv run tensorboard --logdir runs/cartpole
```
### 4. 测试训练结果
```bash
# 自动寻找最佳策略测试(推荐)
uv run scripts/play.py --env cartpole
# 手动指定策略文件测试
uv run scripts/play.py --env cartpole --policy runs/cartpole/nn/best_agent.pickle
```
> **提示**:系统会自动在 `runs/cartpole/` 目录下寻找最新、最佳的策略文件进行测试。您也可以通过 `--policy` 参数手动指定特定的策略文件。
## 配置参数
倒立摆环境的主要配置参数:
```python
@dataclass
class CartPoleEnvCfg(EnvCfg):
model_file: str = "path/to/inverted_pendulum.xml" # MJCF模型文件
reset_noise_scale: float = 0.01 # 重置噪声
max_episode_seconds: float = 10.0 # 最大episode长度
```
训练配置参数:
```python
@dataclass
class CartPoleRLCfg(BaseRLCfg):
num_envs: int = 2048 # 并行环境数量
learning_rate: float = 3e-4 # 学习率
batch_size: int = 2048 # 批大小
max_epochs: int = 500 # 最大训练轮数
```
## 自定义训练
您可以通过命令行参数覆盖默认配置:
```bash
uv run scripts/train.py --env cartpole \
--num-envs 1024 \
--train-backend jax \
--sim-backend np
```
## 预期结果
- 杆子角度大部分时间保持在 ±5 度以内
- 小车位移范围适中
## 故障排除
如果训练效果不佳,可以尝试:
1. 调整学习率(尝试 1e-4 到 1e-3
2. 增加环境数量(更多并行训练)
3. 调整奖励函数权重
4. 检查物理参数设置是否合理

View File

@@ -0,0 +1,102 @@
# 二维步行机器人训练示例
二维步行机器人Walker2D是基于 DeepMind Control Suite 的经典机器人控制任务,目标是通过控制机器人关节来实现站立、行走和奔跑。
```{video} /_static/videos/dm_walker.mp4
:poster: _static/images/poster/dm_walker.jpg
:nocontrols:
:autoplay:
:playsinline:
:muted:
:loop:
:width: 100%
```
## 任务描述
Walker2D 是一个二维平面的双足机器人,具有多个关节和执行器:
- **状态空间**:包括机器人各部位的旋转角度、角速度、躯干高度和速度等
- **动作空间**:控制各个关节的力矩
- **奖励函数**:主要由保持站立、前进速度等组成
- **终止条件**:机器人摔倒或关节达到极限位置
### 三种任务模式
1. **dm-stander**: 静止站立任务 (move_speed = 0.0)
```bash
uv run scripts/train.py --env dm-stander
```
2. **dm-walker**: 行走任务 (move_speed = 1.0)
```bash
uv run scripts/train.py --env dm-walker
```
3. **dm-runner**: 奔跑任务 (move_speed = 5.0)
```bash
uv run scripts/train.py --env dm-runner
```
## 配置参数
### 环境配置
```python
@dataclass
class WalkerEnvCfg(EnvCfg):
model_file: str = "walker.xml" # MJCF模型文件
max_episode_seconds: float = 25.0 # 最大episode长度
sim_dt: float = 0.0125 # 仿真时间步
ctrl_dt: float = 0.025 # 控制时间步
move_speed: float = 1.0 # 目标移动速度
stand_height: float = 1.2 # 目标站立高度
```
### 训练配置
```python
@dataclass
class WalkerRLCfg(BaseRLCfg):
num_envs: int = 512 # 并行环境数量
learning_rate: float = 3e-4 # 学习率
batch_size: int = 512 # 批大小
max_epochs: int = 1000 # 最大训练轮数
```
## 奖励函数设计
Walker2D 的奖励函数由以下几个部分组成:
### 基础站立奖励
```python
# 高度奖励:保持躯干在目标高度
# 直立奖励:保持躯干直立
```
### 移动奖励(行走和奔跑任务)
```python
# 速度奖励:追踪目标速度
# 总奖励 = 站立奖励 * 移动权重
```
## 预期结果
1. **dm-stander**
- 躯干高度保持在 1.0-1.4m 范围
- 躯干直立角度偏差小于 15 度
2. **dm-walker**
- 实际行走速度接近 1.0 m/s
- 步态协调,无明显摔倒
3. **dm-runner**
- 奔跑速度达到 4.0-5.0 m/s
- 出现飞行相(双脚同时离地)

View File

@@ -0,0 +1,139 @@
# Unitree GO1 机器人行走训练示例
Unitree GO1 是一个四足机器人平台,本示例展示了如何训练 GO1 在平坦地形上实现稳定的步态行走。
```{video} /_static/videos/go1_walk.mp4
:poster: _static/images/poster/go1_walk.jpg
:nocontrols:
:autoplay:
:playsinline:
:muted:
:loop:
:width: 100%
```
## 任务描述
GO1 四足机器人具有 12 个自由度(每条腿 3 个关节),需要通过深度强化学习学习协调的步态控制:
- **状态空间**48 维,包含机器人线速度、角速度、姿态、关节角度、关节速度、动作和命令等
- **动作空间**12 维,控制各个关节的目标位置(通过 PD 控制器转换为力矩)
- **奖励函数**:复合奖励,包含速度跟踪、姿态稳定、能量效率等多个组件
- **终止条件**:机器人躯干接触地面或其他不稳定状态
### 训练任务
```bash
uv run scripts/train.py --env go1-flat-terrain-walk
```
## 配置参数
### 环境配置
```python
@dataclass
class Go1WalkNpEnvCfg(EnvCfg):
max_episode_seconds: float = 20.0 # 最大episode长度
model_file: str = "scene_motor_actuator.xml"
sim_dt: float = 0.01 # 仿真时间步
ctrl_dt: float = 0.01 # 控制时间步
```
### 控制配置
```python
@dataclass
class ControlConfig:
stiffness = 80 # PD 控制器刚度 [N*m/rad]
damping = 1 # PD 控制器阻尼 [N*m*s/rad]
action_scale = 0.05 # 动作缩放因子
```
### 初始关节角度
```python
default_joint_angles = {
"FL_hip": 0.0, # 前左髋关节
"RL_hip": 0.0, # 后左髋关节
"FR_hip": -0.0, # 前右髋关节
"RR_hip": -0.0, # 后右髋关节
"FL_thigh": 0.9, # 前左大腿
"RL_thigh": 0.9, # 后左大腿
"FR_thigh": 0.9, # 前右大腿
"RR_thigh": 0.9, # 后右大腿
"FL_calf": -1.8, # 前左小腿
"RL_calf": -1.8, # 后左小腿
"FR_calf": -1.8, # 前右小腿
"RR_calf": -1.8, # 后右小腿
}
```
## 奖励函数设计
GO1 的奖励函数是一个复杂的复合函数,包含多个组件:
### 主要奖励组件
```python
reward_config.scales = {
"tracking_lin_vel": 1.0, # 线速度跟踪奖励
"tracking_ang_vel": 0.5, # 角速度跟踪奖励
"feet_air_time": 1.0, # 足部空中时间奖励
"lin_vel_z": -2.0, # Z轴线速度惩罚
"ang_vel_xy": -0.05, # XY轴角速度惩罚
"orientation": -0.0, # 姿态偏离惩罚
"torques": -0.00001, # 力矩消耗惩罚
"dof_acc": -2.5e-7, # 关节加速度惩罚
"action_rate": -0.001, # 动作变化率惩罚
"hip_pos": -1, # 髋关节位置惩罚
"calf_pos": -0.3, # 腿关节位置惩罚
}
```
### 关键奖励函数
#### 速度跟踪奖励
```python
# 跟踪线速度命令xy平面
def _reward_tracking_lin_vel(self, data, commands):
# 跟踪角速度命令(偏航)
def _reward_tracking_ang_vel(self, data, commands):
```
#### 足部空中时间奖励
```python
def _reward_feet_air_time(self, commands, info):
```
## 观察空间构成
GO1 的观察空间为 48 维,包含以下信息:
```python
obs = np.hstack([
noisy_linvel, # 3维局部坐标系线速度
noisy_gyro, # 3维陀螺仪数据
local_gravity, # 3维局部重力方向
noisy_joint_angle, # 12维关节角度相对于默认值
noisy_joint_vel, # 12维关节速度
last_actions, # 12维上一帧动作
command, # 3维速度命令 [vx, vy, vyaw]
])
```
## 运动速度命令生成
训练过程中随机生成速度命令,确保智能体能够跟踪不同的移动速度:
```python
def resample_commands(self, num_envs: int):
```
## 预期训练结果
1. 稳定的四足步态
2. 良好的速度跟踪

View File

@@ -0,0 +1,84 @@
# 快速入门Hello MotrixLab
本教程通过演示一个简单例子 - 加载倒立摆并进行训练,以此来展示 MotrixLab 工作流程:
## 环境预览
我们提供了一个简单的脚本,用于可视化一个环境,而不执行任何训练,这可以帮助您检测系统的环境依赖是否配置正确:
```bash
uv run scripts/view.py --env cartpole
```
这将打开一个可视化窗口,显示倒立摆的物理仿真环境,使用随机动作进行演示。
## 训练模型
开始训练倒立摆平衡任务:
```bash
uv run scripts/train.py --env cartpole
```
训练过程会自动:
1. 根据硬件环境自动选择训练后端JAX 或 PyTorch
2. 创建训练环境
3. 开始 PPO 算法训练
训练结果会保存在 `runs/cartpole/` 目录下,包含:
- 训练检查点checkpoint
- TensorBoard 日志文件
## 可视化训练过程
如果您想要在训练过程中观察模型的学习过程,可以启用可视化渲染:
```bash
uv run scripts/train.py --env cartpole --render
```
### 🎮 交互式渲染控制
> **重要提示**:可视化会显著降低训练速度,建议主要用于调试和演示。
在可视化训练过程中,您可以使用**空格键**来动态控制渲染:
- **开启渲染**:按下空格键开启可视化,观察机器人行为
- **关闭渲染**:再次按下空格键关闭渲染,提升训练速度
- **随时切换**:无需重新启动程序,可以在训练过程中随时切换
这种交互式控制让您可以在需要时观察训练效果,在不需要时享受快速训练。这项功能在运行推断时也能生效。
## 查看训练结果
使用 TensorBoard 查看训练进度:
```bash
uv run tensorboard --logdir runs/cartpole
```
## 测试训练好的模型
训练完成后,使用训练好的策略进行测试:
```bash
# 自动寻找最佳策略测试(推荐)
uv run scripts/play.py --env cartpole
# 手动指定策略文件测试(如果需要特定版本)
uv run scripts/play.py --env cartpole --policy runs/cartpole/YOUR_RESULT_NUMBER/best_agent.pickle
```
> **提示**:系统会自动在 `runs/cartpole/` 目录下寻找最新、最佳的策略文件。通常情况下,使用自动发现功能即可。
## 至此我们完成了整个示例
接下来可以尝试修改参数,观察不同设置下的物理效果,或者尝试其他环境。
## 下一步
- 了解 [基础框架](../tutorial/basic_frame.md)
- 学习 [物理环境配置](../tutorial/physics_environment.md)
- 查看更多 [训练示例](../demo/cartpole.md)

View File

@@ -0,0 +1,57 @@
# 安装环境
## 安装要求
- **Python 版本**{bdg-danger-line}`3.10.*`
| Python 版本 | 支持状态 |
| :---------: | :------: |
| ≤ 3.9 | ❌ |
| 3.10 | ✅ |
| ≥ 3.11 | ❌ |
- **包管理器**{bdg-danger-line}`UV`
[UV 安装参考](https://docs.astral.sh/uv/getting-started/installation/)
- **系统及架构**
- {bdg-danger-line}`Windows(x86_64)`
- {bdg-danger-line}`Linux(x86_64)`
```{note}
各平台支持的功能如下:
| 操作系统 | CPU 仿真 | 交互式查看器 | GPU 仿真 |
| :------: | :------: | :----------: | :------: |
| Linux | ✅ | ✅ | 🛠️ 开发中 |
| Windows | ✅ | ✅ | 🛠️ 开发中 |
```
## 安装方法
### 克隆项目
```bash
git clone https://github.com/Motphys/MotrixLab.git
cd MotrixLab
```
### 安装依赖
使用 UV 安装项目依赖:
```bash
# 安装所有依赖
uv sync --all-packages --all-extras
```
如果只需要安装一种训练后端,可以选择单独安装指定的后端类型:
```bash
# 安装 SKRL JAX (仅支持 Linux 平台)
uv sync --all-packages --extra skrl-jax
# 安装 SKRL PyTorch
uv sync --all-packages --extra skrl-torch
```

View File

@@ -0,0 +1,32 @@
# 用户指南
```{toctree}
:caption: 入门指南
:maxdepth: 1
getting_started/installation
getting_started/hello_motrixlab
```
```{toctree}
:caption: 使用教程
:maxdepth: 1
tutorial/basic_frame
tutorial/physics_environment
tutorial/training_environment_config
tutorial/rewards
tutorial/training_and_result
```
```{toctree}
:caption: 训练示例
:maxdepth: 1
demo/cartpole
demo/dm_walker
demo/locomotion_unitree_go1
```

View File

@@ -0,0 +1,149 @@
# 基础框架
MotrixLab 是一个机器人强化学习平台,这一节我们会介绍 MotrixLab 的框架设计以及各个组成部分之间的关系。如果您已经熟悉强化学习的内容,可以直接跳转至下一节,了解如何开发自己的训练环境。
## MotrixLab 的框架设计
MotrixLab 采用分层架构设计,将训练环境与训练逻辑进行了清晰拆分:
```
MotrixLab/
├── motrix_envs/ # 环境层:物理仿真和任务定义
│ ├── basic/ # 基础环境cartpole、walker等
│ ├── locomotion/ # 运动环境GO1机器人等
│ ├── np/ # NumPy仿真后端框架
│ ├── base.py # 环境基类
│ └── registry.py # 环境注册系统
├── motrix_rl/ # 训练层RL算法和配置
│ ├── skrl/ # SKRL框架集成JAX/PyTorch
│ ├── base.py # RL配置基类
│ └── registry.py # RL配置注册系统
└── scripts
├── train.py # 训练入口脚本
├── play.py # 测试入口脚本
└── view.py # 可视化脚本
```
## 核心组件架构
```
┌─────────────────────────────────────────────────────────────────┐
│ 用户接口层 │
│ train.py │ play.py │ view.py │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ 训练算法层 (SKRL) │
│ PPO训练器 │ 网络架构 │ 优化器 │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ 环境实现层 │
│ 环境配置(EnvCfg) │ 环境实现(Env) │ 奖励函数(Reward) │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ 物理仿真层 (MotrixSim) │
│ MJCF模型 │ 物理引擎 │ 碰撞检测 │
└─────────────────────────────────────────────────────────────────┘
```
## 核心组件详解
### 1. 训练环境 (Training Environment)
**位置**:环境实现层
训练环境是 MotrixLab 的核心组件,包含三个关键部分:
- **环境配置 (EnvCfg)**定义物理仿真参数模型文件、时间步长、episode 长度等)和任务特定参数
- **环境实现 (Env)**:继承基础环境类,实现具体的任务逻辑、物理仿真交互和终止条件检查
- **奖励函数 (Reward)**:在环境的 step 方法中实现,根据当前状态和动作计算奖励值
环境通过装饰器注册到系统中。
### 2. 奖励函数 (Reward Function)
**位置**:配置管理层 + 环境实现层
奖励函数在 MotrixLab 中采用双重结构设计:
- **配置层面**:在配置类中定义奖励权重、奖励组件类型和缩放参数
- **实现层面**:在环境的 `_compute_reward` 方法中根据配置参数计算具体奖励值
这种设计使得奖励函数既可以通过配置文件灵活调整,又能在代码中实现复杂的计算逻辑。
### 3. 配置参数 (Configuration Parameters)
**位置**:配置管理层
配置参数采用分层管理结构:
- **环境配置 (EnvCfg)**:控制物理仿真和任务行为,包括仿真参数、重置噪声、时间限制等
- **训练配置 (RLCfg)**:控制强化学习算法,包括网络结构、学习率、批次大小、训练步数等
配置类支持继承、参数验证和运行时覆盖,确保参数的合理性和灵活性。
### 4. 注册系统 (Registry System)
**位置**:连接各组件的枢纽
注册系统通过装饰器模式实现组件的自动注册:
- 环境配置类通过 `@registry.envcfg()` 注册
- 环境实现类通过 `@registry.env()` 注册,支持多后端
- RL 配置类通过 `@rlcfg()` 注册
注册系统实现了组件的解耦,使得新增环境或修改配置变得简单快捷。
## 数据流和工作流程
### 训练流程概览
```
用户命令 → 配置解析 → 环境创建 → 训练循环 → 模型保存
train.py --env cartpole
查找配置类 → 创建环境 → 启动PPO训练 → 保存模型
```
### 核心工作流程
1. **环境定义**:在 `/motrix_envs/` 中创建环境配置类和实现类
2. **自动注册**:通过装饰器将组件注册到系统中
3. **配置加载**:命令行启动时,系统自动查找并加载对应的配置
4. **环境创建**:工厂模式创建环境实例,支持参数覆盖
5. **训练执行**PPO 算法与环境交互,收集数据并更新策略
6. **结果保存**:定期保存检查点和最终模型
### 配置参数的作用
配置参数在整个流程中起到关键的连接作用:
- **环境配置**决定物理仿真行为(时间步长、模型文件、噪声等)
- **奖励配置**影响学习信号(奖励权重、计算方式等)
- **训练配置**控制算法行为(网络结构、学习率、批次大小等)
## 多后端支持
MotrixLab 的分层设计天然支持多种后端:
- **仿真后端**MotrixSim
- **训练后端**JAX 和 PyTorch支持 GPU 加速
- **算法框架**:主要集成 SKRL易于扩展其他算法
## 设计优势
这种架构设计带来了以下核心优势:
1. **模块解耦**:环境开发与训练逻辑完全分离
2. **配置灵活**:支持分层配置和运行时参数覆盖
3. **扩展性强**:通过注册系统轻松添加新组件
4. **多后端兼容**:同一环境可使用不同仿真和训练后端
5. **实验友好**:配置可保存、比较,确保实验可重现
通过这个框架设计MotrixLab 为机器人强化学习提供了一个清晰、灵活且易用的开发平台。

View File

@@ -0,0 +1,61 @@
# 物理环境配置
物理环境配置定义了强化学习训练中的仿真参数和模型文件设置。
MotrixLab 使用了[MotrixSim](https://motrixsim.readthedocs.io/zh-cn/latest/user_guide/index.html)作为物理仿真后端。
## 支持的文件格式
- [**MJCF**](https://mujoco.readthedocs.io/en/stable/XMLreference.html)(MuJoCo XML 格式) - 提供丰富的物理特性和仿真配置
## 模型文件配置
需要在环境配置类中指定模型文件路径:
```python
@registry.envcfg("my-task")
@dataclass
class MyTaskEnvCfg(EnvCfg):
# 模型文件路径(必需)
model_file: str = "my_model.xml"
# 仿真时间参数
sim_dt: float = 0.002 # 仿真时间步
ctrl_dt: float = 0.02 # 控制更新频率
```
### 推荐目录结构
```
motrix_envs/my_task/
├── __init__.py # 模块初始化
├── cfg.py # 环境配置
├── my_model.xml # 物理模型文件
└── my_env.py # 环境实现
```
对于结构复杂,引用文件较多的模型,推荐使用文件夹管理。
## 常见配置问题
### 文件路径问题
- 使用相对路径时,确保路径相对于配置文件位置
- 避免使用硬编码的绝对路径
- 检查文件权限和可访问性
- 确保所有引用的子文件都存在
### 时间步设置
- `ctrl_dt` 应该是 `sim_dt` 的整数倍
- `sim_dt` 过小会影响仿真性能
- `ctrl_dt` 过大会影响控制精度
- 推荐 `sim_dt` 在 0.001-0.02 秒之间
### 仿真稳定性
- 避免过大的时间步长
- 合理设置接触参数避免穿透
- 质量和惯性分布要合理
- 关节限制要符合实际情况
通过合理的物理环境配置,您可以为强化学习训练创建准确且高效的仿真环境。

View File

@@ -0,0 +1,50 @@
# 奖励函数设计
奖励函数告诉智能体什么样的行为是期望的,是强化学习环境设计中的核心部分。
## 奖励函数在训练循环中的位置
在 MotrixLab 的 NpEnv 中,奖励计算发生在 `step` 函数的 `update_state` 阶段:
```python
# NpEnv.step() 的执行流程
def step(self, actions: np.ndarray) -> NpEnvState:
# 1. 准备阶段:清空奖励和状态
self._prev_physics_step() # reward = 0.0, terminated = False, truncated = False
# 2. 应用动作
self._state = self.apply_action(actions, self._state)
# 3. 物理仿真
self.physics_step() # 执行物理仿真
# 4. 更新状态 ← 奖励函数在这里计算
self._state = self.update_state(self._state) # 计算奖励和观察值
# 5. 后续处理
self._update_truncate() # 检查时间截断
self._reset_done_envs() # 重置完成的环境
return self._state
```
您需要在子类的 `update_state` 方法中实现奖励计算逻辑,具体奖励函数设计思路请参考训练示例。
### 奖励组件设计原则
1. **分离关注点**:每个奖励函数负责一个特定的目标
2. **权重配置**:通过配置文件管理不同组件的权重
3. **归一化**:保持奖励值在合理的范围内
4. **平滑性**:避免硬性阈值,使用指数函数等平滑过渡
这种方法使得奖励函数模块化,便于调试和调整各个组件的权重。
## 设计原则
1. **明确的目标导向**:奖励函数应该直接反映任务目标
2. **合理的奖励范围**:避免过大或过小的奖励值,保持训练稳定
3. **平衡探索与利用**:适当奖励接近目标的行为,避免稀疏奖励
4. **避免奖励漏洞**:检查智能体是否可能通过不期望的方式获得高奖励
5. **调试友好**:在开发阶段输出奖励分解信息,便于调优
通过在 `update_state` 方法中正确实现奖励计算,您可以为各种机器人任务设计有效的学习信号。

View File

@@ -0,0 +1,79 @@
# 训练执行和结果分析
本节介绍如何执行强化学习训练,以及如何分析和使用训练结果。
## 启动训练
### 基本训练命令
```bash
# 使用默认参数训练
uv run scripts/train.py --env cartpole
# 指定仿真后端
uv run scripts/train.py --env cartpole --sim-backend np
# 指定训练后端
uv run scripts/train.py --env cartpole --train-backend jax
uv run scripts/train.py --env cartpole --train-backend torch
```
### 高级训练配置
```bash
# 自定义训练参数
uv run scripts/train.py --env cartpole \
--num-envs 1024 \
--train-backend jax \
--sim-backend np
# 注意:学习率等参数需要通过配置文件或代码覆盖设置
# 启用渲染监控训练过程
uv run scripts/train.py --env cartpole --render
```
### 支持的命令行参数
| 参数 | 说明 | 默认值 |
| ----------------- | -------------------- | ---------- |
| `--env` | 环境名称 | `cartpole` |
| `--sim-backend` | 仿真后端 (np) | 自动选择 |
| `--train-backend` | 训练后端 (jax/torch) | 自动选择 |
| `--num-envs` | 并行环境数量 | 2048 |
| `--render` | 启用渲染 | False |
> **注意**: 其他参数如学习率、网络结构等需要通过单独文件设置。
## 训练过程监控
### TensorBoard 监控
启动 TensorBoard 查看训练进度:
```bash
uv run tensorboard --logdir runs/{env-name}
```
例如:
```bash
uv run tensorboard --logdir runs/cartpole
```
## 模型评估和测试
### 使用训练好的策略
```bash
# 自动寻找最佳策略测试(推荐)
uv run scripts/play.py --env cartpole
# 手动指定策略文件测试
uv run scripts/play.py --env cartpole --policy runs/cartpole/nn/best_agent.pickle
# 指定测试环境数量
uv run scripts/play.py --env cartpole --num-envs 100
```
> **说明**:系统会自动在 `runs/cartpole/` 目录下寻找最新、最佳的策略文件进行测试。

View File

@@ -0,0 +1,178 @@
# 训练环境配置
MotrixLab 提供了灵活的配置系统,允许用户自定义强化学习训练参数。本节介绍如何配置训练环境和强化学习算法参数。
## RL 训练配置 (PPOCfg)
训练配置定义了基于 PPO 算法的强化学习算法的参数。MotrixLab 现在支持为不同训练后端配置不同的参数。
### 完整配置示例
```python
@dataclass
class CompletePPOConfig(PPOCfg):
"""
完整的强化学习训练配置示例
包含了从基础到高级的所有配置参数
"""
# ===== 基础训练参数 =====
seed: Optional[int] = None # 随机种子
num_envs: int = 2048 # 训练时并行环境数量
play_num_envs: int = 16 # 评估时并行环境数量
max_env_steps: int = 2_048_000 # 最大训练步数
check_point_interval: int = 1000 # 检查点保存间隔
# ===== PPO算法核心参数 =====
learning_rate: float = 3e-4 # 学习率
rollouts: int = 32 # 经验回放轮数
learning_epochs: int = 2 # 每次更新的训练轮数
mini_batches: int = 32 # 小批量数量
discount_factor: float = 0.99 # 折扣因子
lambda_param: float = 0.95 # GAE参数
grad_norm_clip: float = 1.0 # 梯度裁剪
# ===== PPO裁剪参数 =====
ratio_clip: float = 0.2 # PPO裁剪比率
value_clip: float = 0.2 # 价值裁剪
clip_predicted_values: bool = True # 裁剪预测值
# ===== 损失函数参数 =====
entropy_loss_scale: float = 0.0 # 熵损失系数
value_loss_scale: float = 2.0 # 价值损失系数
kl_threshold: float = 0 # KL散度阈值
# ===== 学习率调度器 =====
learning_rate_scheduler_kl_threshold: float = 0.008 # 自适应学习率KL阈值
# ===== 网络架构配置 =====
# 小型网络(适合简单任务如 CartPole
# policy_hidden_layer_sizes: tuple[int, ...] = (128, 64)
# value_hidden_layer_sizes: tuple[int, ...] = (128, 64)
# 中型网络(默认配置,适合大部分任务)
policy_hidden_layer_sizes: tuple[int, ...] = (256, 128, 64)
value_hidden_layer_sizes: tuple[int, ...] = (256, 128, 64)
# 大型网络(适合复杂任务如机器人控制)
# policy_hidden_layer_sizes: tuple[int, ...] = (512, 256, 128)
# value_hidden_layer_sizes: tuple[int, ...] = (512, 256, 128)
# ===== 网络共享配置 =====
share_policy_value_features: bool = True # 策略和价值网络共享特征提取层
# ===== 训练控制参数 =====
random_timesteps: int = 0 # 随机步数
learning_starts: int = 0 # 开始学习的步数
time_limit_bootstrap: bool = True # 时间限制引导
# ===== 奖励整形 =====
rewards_shaper_scale: float = 1.0 # 奖励缩放因子
```
## 配置使用方法
### 1. 默认配置使用
```bash
# 使用代码中给定的配置
uv run scripts/train.py --env my-task
# 指定训练后端,系统会自动选择对应的后端配置
uv run scripts/train.py --env my-task --train-backend jax
uv run scripts/train.py --env my-task --train-backend torch
```
### 2. 命令行参数覆盖
```bash
# 覆盖支持的命令行参数
uv run scripts/train.py --env my-task \
--num-envs 1024 \
--train-backend jax \
--sim-backend np
# 系统会自动选择JAX后端对应的配置
```
### 3. 配置优先级
系统按以下优先级选择配置:
1. **后端特定配置**: 如果存在 `@rlcfg(env_name, backend="jax/torch")` 装饰的配置
2. **通用配置**: 如果存在 `@rlcfg(env_name)` 装饰的配置(无 backend 参数)
例如:
```python
# 最高优先级 - 后端特定配置
@rlcfg("my-task", backend="jax")
@dataclass
class MyTaskJAXCfg(PPOCfg):
mini_batches: int = 4
# 次优先级 - 通用配置
@rlcfg("my-task")
@dataclass
class MyTaskRLCfg(PPOCfg):
mini_batches: int = 32
# 当使用 --train-backend jax 时,系统会选择 MyTaskJAXCfg
# 当使用 --train-backend torch 时,系统会选择 MyTaskRLCfg
```
## SKRL 框架配置映射
在 MotrixLab 中,用户通过 `PPOCfg` 配置类设置参数,这些参数会被映射到 SKRL 框架的配置字典中。
### 用户可配置参数
| MotrixLab 配置类 | SKRL 框架参数 | 说明 |
| -------------------------------------- | --------------------------------------------- | -------------------- |
| `learning_rate` | `learning_rate` | 学习率 |
| `rollouts` | `rollouts` | 经验回放轮数 |
| `learning_epochs` | `learning_epochs` | 训练轮数 |
| `mini_batches` | `mini_batches` | 小批量数量 |
| `discount_factor` | `discount_factor` | 折扣因子 |
| `grad_norm_clip` | `grad_norm_clip` | 梯度裁剪 |
| `lambda_param` | `lambda` | GAE 参数 |
| `ratio_clip` | `ratio_clip` | PPO 裁剪比率 |
| `value_clip` | `value_clip` | 价值裁剪 |
| `clip_predicted_values` | `clip_predicted_values` | 裁剪预测值 |
| `entropy_loss_scale` | `entropy_loss_scale` | 熵损失系数 |
| `value_loss_scale` | `value_loss_scale` | 价值损失系数 |
| `kl_threshold` | `kl_threshold` | KL 散度阈值 |
| `random_timesteps` | `random_timesteps` | 随机步数 |
| `learning_starts` | `learning_starts` | 开始学习的步数 |
| `time_limit_bootstrap` | `time_limit_bootstrap` | 时间限制引导 |
| `learning_rate_scheduler_kl_threshold` | `learning_rate_scheduler_kwargs.kl_threshold` | 自适应学习率 KL 阈值 |
| `check_point_interval` | `experiment.write_interval` | 日志写入间隔 |
| `check_point_interval` | `experiment.checkpoint_interval` | 检查点保存间隔 |
| `rewards_shaper_scale` | `rewards_shaper` | 奖励缩放函数 |
### 预处理器参数
| SKRL 框架参数 | 类型 | 说明 |
| -------------------- | --------------------- | ---------- |
| `state_preprocessor` | RunningStandardScaler | 状态标准化 |
| `value_preprocessor` | RunningStandardScaler | 价值标准化 |
### 配置层次总结
```
用户配置类 (PPOCfg)
↓ 后端特定选择
后端配置 (JAX/Torch)
↓ 参数映射
SKRL 框架配置字典
↓ 传递给
PPO Agent
↓ 执行
强化学习训练
```
这种设计允许用户:
1. 通过简单的配置类来控制复杂的训练参数
2. 为不同训练后端配置不同的参数以获得最佳性能
3. 保持与 SKRL 框架的完全兼容性

172
motrix_envs/README.md Normal file
View File

@@ -0,0 +1,172 @@
# Motrix Environments (Motrix_envs)
motrix Environments 是一个基于 MotrixSim 仿真后端的强化学习环境库,提供了与具体 RL 框架无关的仿真环境定义。该模块设计上支持多种仿真后端,目前主要实现了 MotrixSim 的 NumPy 后端。
## 📁 项目结构
```
motrix_envs/
├── base.py # 基础抽象类和配置
├── registry.py # 环境注册系统
├── np/ # NumPy 仿真后端实现
│ ├── env.py # NumPy 环境基类
│ ├── renderer.py # 渲染器
│ └── reward.py # 奖励函数
├── basic/ # 基础环境
│ ├── cartpole/ # 倒立摆环境
│ └── walker/ # 步行者环境
├── locomotion/ # 运动控制环境
│ └── go1/ # GO1 机器人
│ ├── xmls/ # 机器人模型文件
│ ├── walk_np.py # GO1 行走实现
│ └── cfg.py # GO1 配置
└── common/ # 公共组件
```
## 🎯 内置环境
| 环境名称 | 注册标识符 | 后端 | 类型 | 描述 |
| ---------------- | ----------------------- | ---- | ---------- | ----------------------------- |
| **倒立摆** | `cartpole` | np | Basic | 经典控制任务,保持杆子平衡 |
| **步行者** | `walker` | np | Basic | 平面双足步行机器人控制 |
| **GO1 平地行走** | `go1-flat-terrain-walk` | np | Locomotion | 四足机器人 GO1 的平地行走任务 |
### 详细说明
#### 1. CartPole (倒立摆) - `cartpole`
- **观测空间 (4 维)**:
- `cart_pos`: 小车位置 [-0.8, 0.8]
- `pole_angle`: 杆子角度 [-0.2, 0.2]
- `cart_vel`: 小车速度
- `pole_vel`: 杆子角速度
- **动作空间 (1 维)**: 推力 `[-3.0, 3.0]`
- **奖励**: 每步 +1.0
- **终止条件**: 杆子角度 > 0.2 弧度 或 小车位置超出边界
- **配置参数**: `reset_noise_scale=0.01`
#### 2. GO1 平地行走 - `go1-flat-terrain-walk`
- **观测空间 (48 维)**: 包含关节位置、速度、IMU 数据、命令等
- **动作空间 (12 维)**: 12 个关节的目标角度控制
- **奖励组件**:
- `tracking_lin_vel`: 线速度跟踪 (权重: 1.0)
- `tracking_ang_vel`: 角速度跟踪 (权重: 0.5)
- `feet_air_time`: 足部空中时间 (权重: 1.0)
- `collision`: 碰撞惩罚 (权重: -1.0)
- `action_rate`: 动作变化率惩罚 (权重: -0.001)
- **控制参数**: 刚度 80.0 Nm/rad, 阻尼 1.0 Nms/rad
- **噪声模型**: 关节角度、速度、陀螺仪等多种传感器噪声
## 🛠️ 自定义环境
### 开发步骤
#### 1. 定义环境配置类
```python
from dataclasses import dataclass
from motrix_envs import registry
from motrix_envs.base import EnvCfg
@registry.envcfg("my-custom-env")
@dataclass
class MyEnvCfg(EnvCfg):
"""自定义环境配置"""
# 继承基础配置
reset_noise_scale: float = 0.01
max_episode_seconds: float = 10.0
# 添加自定义参数
custom_param_1: float = 1.0
custom_param_2: str = "default_value"
enable_feature_x: bool = True
```
#### 2. 实现环境类
```python
import gymnasium as gym
import numpy as np
from motrix_envs import registry
from motrix_envs.np.env import NpEnv, NpEnvState
@registry.env("my-custom-env", "np")
class MyCustomEnv(NpEnv):
"""自定义环境实现"""
def __init__(self, cfg: MyEnvCfg, num_envs: int = 1):
super().__init__(cfg, num_envs=num_envs)
@property
def observation_space(self):
"""返回观测空间"""
raise NotImplementedError
@property
def action_space(self):
"""返回动作空间"""
raise NotImplementedError
def apply_action(self, actions: np.ndarray, state: NpEnvState):
"""将actions应用到环境状态"""
raise NotImplementedError
return state
def update_state(self, state: NpEnvState):
"""更新环境状态,计算观测、奖励、终止条件"""
# 提取仿真数据
data = state.data
# 计算观测
obs = self._compute_observation(data)
# 计算奖励
reward = self._compute_reward(data, obs)
# 检查终止条件
terminated = self._check_termination(data, obs)
# 更新状态
state.obs = obs
state.reward = reward
state.terminated = terminated
return state
def reset(self, data: mtx.SceneData) -> tuple[np.ndarray, dict]:
"""
重置环境
参数:
data: motrixsim 场景数据对象,仅包含需要重置的环境实例
返回:
obs: 重置后场景的初始观测
info: 额外信息
"""
raise NotImplementedError
```
#### 3. 注册和使用环境
```python
# 确保导入了环境模块,触发注册
import motrix_envs.basic.my_custom_env # noqa: F401
from motrix_envs import registry
# 创建环境实例
env = registry.make(
name="my-custom-env",
sim_backend="np",
num_envs=256,
env_cfg_override={
"custom_param_1": 2.0,
"reset_noise_scale": 0.02
}
)
# 使用环境
for step in range(1000):
actions = sample_actions_somehow()
state = env.step(actions)
```

View File

@@ -0,0 +1,15 @@
[build-system]
requires = ["uv_build>=0.9.6,<0.10.0"]
build-backend = "uv_build"
[project]
name = "motrix-envs"
version = "0.0.1"
description = "Robot simulation environment library based on MotrixSim providing multi-task RL environments."
authors = [{ name = "Motphys", email = "developers@motphys.com" }]
requires-python = "==3.10.*"
readme = "README.md"
license = "Apache-2.0"
dependencies = [
"motrixsim>=0.4.0",
]

View File

@@ -0,0 +1,16 @@
# Copyright (C) 2020-2025 Motphys Technology Co., Ltd. All Rights Reserved.
#
# 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.
# ==============================================================================
from . import basic, locomotion # noqa: F401

View File

@@ -0,0 +1,82 @@
# Copyright (C) 2020-2025 Motphys Technology Co., Ltd. All Rights Reserved.
#
# 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.
# ==============================================================================
import abc
from dataclasses import dataclass
from typing import Optional
import gymnasium as gym
@dataclass
class EnvCfg:
"""
Config for the environment
"""
model_file: str = None
sim_dt: float = 0.01
max_episode_seconds: float = None
ctrl_dt: float = 0.01
@property
def max_episode_steps(self) -> Optional[int]:
"""
return the max episode steps
"""
if self.max_episode_seconds is None:
return None
return int(self.max_episode_seconds / self.ctrl_dt)
@property
def sim_substeps(self) -> int:
"""
return the number of simulation steps per control step
"""
return int(round(self.ctrl_dt / self.sim_dt))
def validate(self):
"""
validate the config
"""
if self.sim_dt > self.ctrl_dt:
raise ValueError("sim_dt must be less than or equal to ctrl_dt")
class ABEnv(abc.ABC):
@property
@abc.abstractmethod
def num_envs(self) -> int:
"""
return the size of the env if it is vectorized
"""
@property
@abc.abstractmethod
def cfg(self) -> EnvCfg:
"""
The configuration of the environment
"""
@property
@abc.abstractmethod
def observation_space(self) -> gym.Space:
"""Observation space"""
@property
@abc.abstractmethod
def action_space(self) -> gym.Space:
"""Action space"""

View File

@@ -0,0 +1,16 @@
# Copyright (C) 2020-2025 Motphys Technology Co., Ltd. All Rights Reserved.
#
# 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.
# ==============================================================================
from . import cartpole, walker # noqa: F401 import to register envs

View File

@@ -0,0 +1,16 @@
# Copyright (C) 2020-2025 Motphys Technology Co., Ltd. All Rights Reserved.
#
# 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.
# ==============================================================================
from . import cartpole_np # noqa: F401

View File

@@ -0,0 +1,36 @@
<mujoco model="cartpole">
<compiler inertiafromgeom="true" />
<option gravity="0 0 -9.81" integrator="RK4" timestep="0.01" />
<default>
<joint armature="0" damping="1" limited="true" solimplimit="0 0 0.001 0.5 1.0" />
<geom contype="0" conaffinity="0" friction="1 0.1 0.1" />
<motor ctrlrange="-3 3" />
</default>
<asset>
<texture type="2d" name="groundplane" builtin="checker" mark="edge"
rgb1="0.4314 0.5294 0.6431" rgb2="0.8157 0.8549 0.9059"
markrgb="0.8 0.8 0.8" width="300" height="300" />
<material name="groundplane" texture="groundplane" texuniform="true" texrepeat="1 1"
reflectance="0.2" />
</asset>
<worldbody>
<light pos="0 0 1.5" dir="0 0 -1" directional="true" />
<geom name="floor" size="0 0 0.01" type="plane" material="groundplane" pos="0 0 -1" />
</worldbody>
<worldbody>
<geom name="rail" pos="0 0 0" quat="0.707 0 0.707 0" rgba="0.3 0.3 0.7 1" size="0.02 1"
type="capsule" />
<body name="cart" pos="0 0 0">
<joint axis="1 0 0" limited="true" name="slider" pos="0 0 0" range="-1 1" type="slide" />
<geom name="cart" pos="0 0 0" quat="0.707 0 0.707 0" size="0.1 0.1" type="capsule" />
<body name="pole" pos="0 0 0">
<joint axis="0 1 0" name="hinge" pos="0 0 0" range="-90 90" type="hinge" />
<geom pos="0 0 0.3" name="cpole" rgba="0 0.7 0.7 1" size="0.049 0.3" type="capsule" />
</body>
</body>
</worldbody>
<actuator>
<motor ctrllimited="true" ctrlrange="-3 3" gear="100" joint="slider" name="slide" />
</actuator>
</mujoco>

View File

@@ -0,0 +1,97 @@
# Copyright (C) 2020-2025 Motphys Technology Co., Ltd. All Rights Reserved.
#
# 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.
# ==============================================================================
import gymnasium as gym
import motrixsim as mtx
import numpy as np
from motrix_envs import registry
from motrix_envs.np.env import NpEnv, NpEnvState
from .cfg import CartPoleEnvCfg
@registry.env("cartpole", "np")
class CartPoleEnv(NpEnv):
_cfg: CartPoleEnvCfg
def __init__(self, cfg: CartPoleEnvCfg, num_envs: int = 1):
super().__init__(cfg, num_envs=num_envs)
self._action_space = gym.spaces.Box(-3.0, 3.0, (1,), dtype=np.float32)
self._observation_space = gym.spaces.Box(-np.inf, np.inf, (4,), dtype=np.float32)
self._num_dof_pos = self._model.num_dof_pos
self._num_dof_vel = self._model.num_dof_vel
self._init_dof_pos = self._model.compute_init_dof_pos()
self._init_dof_vel = np.zeros(
(self._model.num_dof_vel,),
dtype=np.float32,
)
@property
def observation_space(self):
return self._observation_space
@property
def action_space(self):
return self._action_space
def apply_action(self, actions: np.ndarray, state: NpEnvState):
state.data.actuator_ctrls = actions
return state
def update_state(self, state: NpEnvState):
# compute observation
data = state.data
dof_pos = data.dof_pos
dof_vel = data.dof_vel
obs = np.concatenate([dof_pos, dof_vel], axis=-1)
assert obs.shape == (self._num_envs, 4)
# compute reward
reward = np.ones((self._num_envs,), dtype=np.float32)
# compute terminated
cart_pos = dof_pos[:, 0]
angle = dof_pos[:, 1]
terminated = np.logical_or(np.isnan(angle), np.abs(angle) > 0.2)
terminated = np.logical_or(cart_pos < -0.8, terminated)
terminated = np.logical_or(cart_pos > 0.8, terminated)
state.obs = obs
state.reward = reward
state.terminated = terminated
return state
def reset(self, data: mtx.SceneData):
cfg: CartPoleEnvCfg = self._cfg
noise_pos = np.random.uniform(
-cfg.reset_noise_scale,
cfg.reset_noise_scale,
(*data.shape, self._num_dof_pos),
)
noise_vel = np.random.uniform(
-cfg.reset_noise_scale,
cfg.reset_noise_scale,
(*data.shape, self._num_dof_vel),
)
dof_pos = np.tile(self._init_dof_pos, (*data.shape, 1)) + noise_pos
dof_vel = np.tile(self._init_dof_vel, (*data.shape, 1)) + noise_vel
data.reset(self._model)
data.set_dof_vel(dof_vel)
data.set_dof_pos(dof_pos, self._model)
obs = np.concatenate([dof_pos, dof_vel], axis=-1)
return obs, {}

View File

@@ -0,0 +1,30 @@
# Copyright (C) 2020-2025 Motphys Technology Co., Ltd. All Rights Reserved.
#
# 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.
# ==============================================================================
import os
from dataclasses import dataclass
from motrix_envs import registry
from motrix_envs.base import EnvCfg
model_file = os.path.dirname(__file__) + "/cartpole.xml"
@registry.envcfg("cartpole")
@dataclass
class CartPoleEnvCfg(EnvCfg):
model_file: str = model_file
reset_noise_scale: float = 0.01
max_episode_seconds: float = 10

View File

@@ -0,0 +1,16 @@
# Copyright (C) 2020-2025 Motphys Technology Co., Ltd. All Rights Reserved.
#
# 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.
# ==============================================================================
from . import walker_np # noqa: F401

View File

@@ -0,0 +1,45 @@
# Copyright (C) 2020-2025 Motphys Technology Co., Ltd. All Rights Reserved.
#
# 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.
# ==============================================================================
import os
from dataclasses import dataclass
from motrix_envs import registry
from motrix_envs.base import EnvCfg
model_file = os.path.dirname(__file__) + "/walker.xml"
@registry.envcfg("dm-walker")
@dataclass
class WalkerEnvCfg(EnvCfg):
model_file: str = model_file
max_episode_seconds: float = 25.0
sim_dt: float = 0.0125
move_speed: float = 1.0
ctrl_dt: float = 0.025
stand_height: float = 1.2
@registry.envcfg("dm-stander")
@dataclass
class StanderEnvCfg(WalkerEnvCfg):
move_speed: float = 0.0
@registry.envcfg("dm-runner")
@dataclass
class RunnerEnvCfg(WalkerEnvCfg):
move_speed: float = 5.0

View File

@@ -0,0 +1,71 @@
<mujoco model="planar walker">
<include file="../../common/visual.xml" />
<include file="../../common/skybox.xml" />
<include file="../../common/materials.xml" />
<option timestep="0.005">
<flag eulerdamp="disable" />
</option>
<statistic extent="2" center="0 0 1" />
<default>
<joint damping=".1" armature="0.01" limited="true" solimplimit="0 .99 .01" />
<geom contype="1" conaffinity="1" friction=".7 0.01 0.01" />
<!-- Note: the original friction is 0.1 -->
<motor ctrlrange="-1 1" ctrllimited="true" />
<site size="0.01" />
<default class="walker">
<geom material="self" type="capsule" />
<joint axis="0 -1 0" />
</default>
</default>
<worldbody>
<light name="light" dir="0 -1 -1" directional="true"/>
<geom name="floor" type="plane" conaffinity="1" pos="248 0 0" size="250 .8 .2" material="grid" zaxis="0 0 1" />
<body name="torso" pos="0 0 1.3" childclass="walker">
<joint name="rootz" axis="0 0 1" type="slide" limited="false" armature="0" damping="0" />
<joint name="rootx" axis="1 0 0" type="slide" limited="false" armature="0" damping="0" />
<joint name="rooty" axis="0 1 0" type="hinge" limited="false" armature="0" damping="0" />
<geom name="torso" size="0.07 0.3" />
<body name="right_thigh" pos="0 -.05 -0.3">
<joint name="right_hip" range="-20 100" />
<geom name="right_thigh" pos="0 0 -0.225" size="0.05 0.225" />
<body name="right_leg" pos="0 0 -0.7">
<joint name="right_knee" pos="0 0 0.25" range="-150 0" />
<geom name="right_leg" size="0.04 0.25" />
<body name="right_foot" pos="0.06 0 -0.25">
<joint name="right_ankle" pos="-0.06 0 0" range="-45 45" />
<geom name="right_foot" zaxis="1 0 0" size="0.05 0.1" />
</body>
</body>
</body>
<body name="left_thigh" pos="0 .05 -0.3">
<joint name="left_hip" range="-20 100" />
<geom name="left_thigh" pos="0 0 -0.225" size="0.05 0.225" />
<body name="left_leg" pos="0 0 -0.7">
<joint name="left_knee" pos="0 0 0.25" range="-150 0" />
<geom name="left_leg" size="0.04 0.25" />
<body name="left_foot" pos="0.06 0 -0.25">
<joint name="left_ankle" pos="-0.06 0 0" range="-45 45" />
<geom name="left_foot" zaxis="1 0 0" size="0.05 0.1" />
</body>
</body>
</body>
</body>
</worldbody>
<sensor>
<subtreelinvel name="torso_subtreelinvel" body="torso" />
</sensor>
<actuator>
<motor name="right_hip" joint="right_hip" gear="100" />
<motor name="right_knee" joint="right_knee" gear="50" />
<motor name="right_ankle" joint="right_ankle" gear="20" />
<motor name="left_hip" joint="left_hip" gear="100" />
<motor name="left_knee" joint="left_knee" gear="50" />
<motor name="left_ankle" joint="left_ankle" gear="20" />
</actuator>
</mujoco>

View File

@@ -0,0 +1,155 @@
# Copyright (C) 2020-2025 Motphys Technology Co., Ltd. All Rights Reserved.
#
# 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.
# ==============================================================================
import gymnasium as gym
import motrixsim as mtx
import numpy as np
from motrix_envs import registry
from motrix_envs.basic.walker.cfg import WalkerEnvCfg
from motrix_envs.np import reward
from motrix_envs.np.env import NpEnv, NpEnvState
@registry.env("dm-walker", "np")
@registry.env("dm-runner", "np")
@registry.env("dm-stander", "np")
class Walker2DEnv(NpEnv):
_observation_space: gym.spaces.Box
_action_space: gym.spaces.Box
def __init__(self, cfg: WalkerEnvCfg, num_envs=1):
super().__init__(cfg, num_envs)
self._init_obs_space()
self._init_action_space()
self._torso = self._model.get_link("torso")
self._move_speed = cfg.move_speed
self._joint_limits = self._model.joint_limits
self._stand_height = cfg.stand_height
def _init_obs_space(self):
model = self._model
num = 0
num += (model.num_links - 1) * 2 # planar rotation (x,z) for each link except the root
num += 1 # torso height
num += model.num_dof_vel
self._observation_space = gym.spaces.Box(-np.inf, np.inf, (num,), dtype=np.float32)
def _init_action_space(self):
model = self._model
self._action_space = gym.spaces.Box(
model.actuator_ctrl_limits[0],
model.actuator_ctrl_limits[1],
(model.num_actuators,),
dtype=np.float32,
)
@property
def observation_space(self) -> gym.spaces.Box:
return self._observation_space
@property
def action_space(self) -> gym.spaces.Box:
return self._action_space
def apply_action(self, actions, state):
state.data.actuator_ctrls = actions
return state
def _get_obs(self, data: mtx.SceneData) -> np.ndarray:
num_env = data.shape[0]
link_rotations = self._model.get_link_rotation_mats(data)
dof_vel = data.dof_vel
up_right = link_rotations[:, 0, 2, 2].reshape(num_env, 1) # 1
orientations = link_rotations[:, 1:, [0, 0], [2, 0]].reshape(num_env, -1) # (num_links - 1) * 2
obs = np.concatenate([orientations, up_right, dof_vel], axis=-1)
return obs
def update_state(self, state: NpEnvState) -> NpEnvState:
data = state.data
# === compute obs ====
obs = self._get_obs(data)
offset = (self._model.num_links - 1) * 2 + 1
dof_vel = obs[:, offset:]
torso_upright = self._torso.get_rotation_mat(data)[:, 2, 2]
torso_height = self._torso.get_position(data)[:, 2]
torso_vel = self._model.get_sensor_value("torso_subtreelinvel", data)
horizontal_velocity = torso_vel[:, 0]
# ==== compute terminated
terminated = np.isnan(dof_vel).any(axis=-1)
# ==== compute reward
rwd_height = reward.tolerance(
torso_height,
bounds=(self._stand_height, float("inf")),
margin=self._stand_height * 4 / 5,
)
rwd_upright = (1 + torso_upright) / 2
rwd_stand = (3 * rwd_height + 1 * rwd_upright) / 4
rwd = rwd_stand
state.info["Reward"] = {
"height": rwd_height,
"upright": rwd_upright,
"stand": rwd_stand,
}
if self._move_speed > 0.0:
rwd_move = reward.tolerance(
horizontal_velocity,
bounds=(self._move_speed, float("inf")),
margin=self._move_speed / 2,
value_at_margin=0.5,
sigmoid="linear",
)
state.info["Reward"]["move"] = rwd_move
rwd = rwd_stand * (5 * rwd_move + 1) / 6
rwd[terminated] = 0.0
return state.replace(
obs=obs,
reward=rwd,
terminated=terminated,
)
def reset(self, data: mtx.SceneData) -> tuple[np.ndarray, dict]:
data.reset(self._model)
num_reset = data.shape[0]
dof_pos = np.zeros((num_reset, self._model.num_dof_pos))
dof_pos[:, 2] = np.random.uniform(low=-np.pi, high=np.pi, size=(num_reset,)) # randomize root yaw
dof_pos[:, 3:] = np.random.uniform(
low=self._joint_limits[0, 3:],
high=self._joint_limits[1, 3:],
size=(num_reset, self._model.num_dof_pos - 3),
) # randomize other joint angles
data.set_dof_pos(dof_pos, self._model)
self._model.forward_kinematic(data)
obs = self._get_obs(data)
rewards = {
"height": np.zeros((num_reset,)),
"upright": np.zeros((num_reset,)),
"stand": np.zeros((num_reset,)),
}
if self._move_speed > 0.0:
rewards["move"] = np.zeros((num_reset,))
return obs, {"Reward": rewards}

View File

@@ -0,0 +1,23 @@
<!--
Common textures, colors and materials to be used throughout this suite. Some
materials such as xxx_highlight are activated on occurence of certain events,
for example receiving a positive reward.
-->
<mujoco>
<asset>
<texture name="grid" type="2d" builtin="checker" mark="edge" rgb1="0.4314 0.5294 0.6431" rgb2="0.8157 0.8549 0.9059" markrgb="0.8 0.8 0.8" width="300" height="300"/>
<material name="grid" texture="grid" texrepeat="1 1" texuniform="true" reflectance=".2"/>
<material name="self" rgba=".7 .5 .3 1"/>
<material name="self_default" rgba=".7 .5 .3 1"/>
<material name="self_highlight" rgba="0 .5 .3 1"/>
<material name="effector" rgba=".7 .4 .2 1"/>
<material name="effector_default" rgba=".7 .4 .2 1"/>
<material name="effector_highlight" rgba="0 .5 .3 1"/>
<material name="decoration" rgba=".3 .5 .7 1"/>
<material name="eye" rgba="0 .2 1 1"/>
<material name="target" rgba=".6 .3 .3 1"/>
<material name="target_default" rgba=".6 .3 .3 1"/>
<material name="target_highlight" rgba=".6 .3 .3 .4"/>
<material name="site" rgba=".5 .5 .5 .3"/>
</asset>
</mujoco>

View File

@@ -0,0 +1,6 @@
<mujoco>
<asset>
<texture name="skybox" type="skybox" builtin="gradient" rgb1=".4 .6 .8" rgb2="0 0 0"
width="800" height="800" mark="random" markrgb="1 1 1"/>
</asset>
</mujoco>

View File

@@ -0,0 +1,7 @@
<mujoco>
<visual>
<headlight ambient=".4 .4 .4" diffuse=".8 .8 .8" specular="0.1 0.1 0.1"/>
<map znear=".01"/>
<quality shadowsize="2048"/>
</visual>
</mujoco>

View File

@@ -0,0 +1,16 @@
# Copyright (C) 2020-2025 Motphys Technology Co., Ltd. All Rights Reserved.
#
# 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.
# ==============================================================================
from . import go1 # noqa: F401 register envs

View File

@@ -0,0 +1,16 @@
# Copyright (C) 2020-2025 Motphys Technology Co., Ltd. All Rights Reserved.
#
# 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.
# ==============================================================================
from . import walk_np # noqa: F401 register envs

View File

@@ -0,0 +1,140 @@
# Copyright (C) 2020-2025 Motphys Technology Co., Ltd. All Rights Reserved.
#
# 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.
# ==============================================================================
import os
from dataclasses import dataclass, field
from motrix_envs import registry
from motrix_envs.base import EnvCfg
model_file = os.path.dirname(__file__) + "/xmls/scene_motor_actuator.xml"
@dataclass
class NoiseConfig:
level: float = 1.0
scale_joint_angle: float = 0.03
scale_joint_vel: float = 1.5
scale_gyro: float = 0.2
scale_gravity: float = 0.05
scale_linvel: float = 0.1
@dataclass
class ControlConfig:
stiffness = 80 # [N*m/rad]
damping = 1 # [N*m*s/rad]
# action scale: target angle = actionScale * action + defaultAngle
action_scale = 0.05
@dataclass
class InitState:
# the initial position of the robot in the world frame
pos = [0.0, 0.0, 0.42]
# the default angles for all joints. key = joint name, value = target angle [rad]
default_joint_angles = {
"FL_hip": 0.0, # [rad]
"RL_hip": 0.0, # [rad]
"FR_hip": -0.0, # [rad]
"RR_hip": -0.0, # [rad]
"FL_thigh": 0.9, # [rad]
"RL_thigh": 0.9, # [rad]
"FR_thigh": 0.9, # [rad]
"RR_thigh": 0.9, # [rad]
"FL_calf": -1.8, # [rad]
"RL_calf": -1.8, # [rad]
"FR_calf": -1.8, # [rad]
"RR_calf": -1.8, # [rad]
}
@dataclass
class Commands:
vel_limit = [
[0.0, -1.0, -1.0], # min: vel_x [m/s], vel_y [m/s], ang_vel [rad/s]
[2.0, 1.0, 1.0], # max
]
@dataclass
class Normalization:
lin_vel = 2
ang_vel = 0.25
dof_pos = 1
dof_vel = 0.05
@dataclass
class Asset:
body_name = "trunk"
foot_name = "foot"
penalize_contacts_on = ["thigh", "calf"]
terminate_after_contacts_on = [
"trunk",
]
ground = "floor"
@dataclass
class Sensor:
local_linvel = "local_linvel"
gyro = "gyro"
@dataclass
class RewardConfig:
scales: dict[str, float] = field(
default_factory=lambda: {
"termination": -0.0,
"tracking_lin_vel": 1.0,
"tracking_ang_vel": 0.5,
"lin_vel_z": -2.0,
"ang_vel_xy": -0.05,
"orientation": -0.0,
"torques": -0.00001,
"dof_vel": -0.0,
"dof_acc": -2.5e-7,
"base_height": -0.0,
"feet_air_time": 1.0,
"collision": -1.0 * 0,
"feet_stumble": -0.0,
"action_rate": -0.001,
"stand_still": -0.0,
"hip_pos": -1,
"calf_pos": -0.3 * 0,
}
)
tracking_sigma: float = 0.25
max_foot_height: float = 0.1
@registry.envcfg("go1-flat-terrain-walk")
@dataclass
class Go1WalkNpEnvCfg(EnvCfg):
max_episode_seconds: float = 20.0
model_file: str = model_file
noise_config: NoiseConfig = field(default_factory=NoiseConfig)
control_config: ControlConfig = field(default_factory=ControlConfig)
reward_config: RewardConfig = field(default_factory=RewardConfig)
init_state: InitState = field(default_factory=InitState)
commands: Commands = field(default_factory=Commands)
normalization: Normalization = field(default_factory=Normalization)
asset: Asset = field(default_factory=Asset)
sensor: Sensor = field(default_factory=Sensor)
sim_dt: float = 0.01
ctrl_dt: float = 0.01

View File

@@ -0,0 +1,413 @@
# Copyright (C) 2020-2025 Motphys Technology Co., Ltd. All Rights Reserved.
#
# 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.
# ==============================================================================
import gymnasium as gym
import motrixsim as mtx
import numpy as np
from motrix_envs import registry
from motrix_envs.locomotion.go1.cfg import Go1WalkNpEnvCfg
from motrix_envs.np.env import NpEnv, NpEnvState
## provide quat math utility from motrixsim.
def quat_rotate_inverse(quats, v):
"""
Rotate a fixed vector v by a list of quaternions using a vectorized approach.
Parameters:
quats (np.ndarray): Array of quaternions of shape (N, 4). Each quaternion is in [w, x, y, z] format.
v (np.ndarray): Fixed vector of shape (3,) to be rotated.
Returns:
np.ndarray: Array of rotated vectors of shape (N, 3).
"""
# Normalize the quaternions to ensure they are unit quaternions
# Extract the scalar (w) and vector (x, y, z) parts of the quaternions
w = quats[:, -1] # Shape (N,)
im = quats[:, :3] # Shape (N, 3)
# Compute the cross product between the imaginary part of each quaternion and the fixed vector v.
# np.cross broadcasts v to match each row in im, resulting in an array of shape (N, 3)
cross_im_v = np.cross(im, v)
# Compute the intermediate terms for the rotation formula:
term1 = w[:, np.newaxis] * cross_im_v # w * cross(im, v)
term2 = np.cross(im, cross_im_v) # cross(im, cross(im, v))
# Apply the rotation formula: v_rot = v + 2 * (term1 + term2)
v_rotated = v + 2 * (term1 + term2)
return v_rotated
@registry.env("go1-flat-terrain-walk", sim_backend="np")
class Go1WalkTask(NpEnv):
_init_dof_pos: np.ndarray
_init_dof_vel: np.ndarray
def __init__(self, cfg: Go1WalkNpEnvCfg, num_envs=1):
super().__init__(cfg, num_envs)
self._init_action_space()
self._init_obs_space()
self._body = self._model.get_body(self.cfg.asset.body_name)
self._num_action = self._action_space.shape[0]
self._num_observation = self._observation_space.shape[0]
self._num_dof_pos = self._model.num_dof_pos
self._num_dof_vel = self._model.num_dof_vel
self._init_dof_vel = np.zeros(
(self._num_dof_vel,),
dtype=np.float32,
)
self._init_dof_pos = self._model.compute_init_dof_pos()
self._init_buffer()
def _init_obs_space(self):
model = self.model
num_dof_vel = model.num_dof_vel # linvel + gyro + joint_vel
num_joint_angle = model.num_dof_pos - 7
num_gravity = 3
num_actions = model.num_actuators
num_command = 3
num_obs = num_dof_vel + num_joint_angle + num_gravity + num_actions + num_command
assert num_obs == 48
self._observation_space = gym.spaces.Box(-np.inf, np.inf, (num_obs,), dtype=np.float32)
def _init_action_space(self):
model = self.model
self._action_space = gym.spaces.Box(
np.array(model.actuator_ctrl_limits[0, :]),
np.array(model.actuator_ctrl_limits[1, :]),
(model.num_actuators,),
dtype=np.float32,
)
@property
def action_space(self) -> gym.spaces.Box:
return self._action_space
@property
def observation_space(self) -> gym.spaces.Box:
return self._observation_space
def get_dof_pos(self, data: mtx.SceneModel):
return self._body.get_joint_dof_pos(data)
def get_dof_vel(self, data: mtx.SceneModel):
return self._body.get_joint_dof_vel(data)
def _init_buffer(self):
cfg = self._cfg
assert isinstance(cfg, Go1WalkNpEnvCfg)
# init buffers
self.reset_buf = np.ones(self._num_envs, dtype=np.bool)
self.kps = np.ones(self._num_action, dtype=np.float32) * cfg.control_config.stiffness
self.kds = np.ones(self._num_action, dtype=np.float32) * cfg.control_config.damping
self.gravity_vec = np.array([0, 0, -1], dtype=np.float32)
self.commands_scale = np.array(
(
[
cfg.normalization.lin_vel,
cfg.normalization.lin_vel,
cfg.normalization.ang_vel,
]
),
dtype=np.float32,
)
self.default_angles = np.zeros(self._num_action, dtype=np.float32)
self.hip_indices = []
self.calf_indices = []
for i in range(self._model.num_actuators):
for name in cfg.init_state.default_joint_angles.keys():
if name in self._model.actuator_names[i]:
self.default_angles[i] = cfg.init_state.default_joint_angles[name]
if "hip" in self._model.actuator_names[i]:
self.hip_indices.append(i)
if "calf" in self._model.actuator_names[i]:
self.calf_indices.append(i)
self._init_dof_pos[-self._num_action :] = self.default_angles
self.ground = self._model.get_geom_index(cfg.asset.ground)
self.termination_contact = None
self.foot = []
for name in cfg.asset.terminate_after_contacts_on:
if self.termination_contact is None:
self.termination_contact = np.array([[self._model.get_geom_index(name), self.ground]], dtype=np.uint32)
else:
self.termination_contact = np.append(
self.termination_contact,
np.array(
[[self._model.get_geom_index(name), self.ground]],
dtype=np.uint32,
),
axis=0,
)
for name in cfg.asset.foot_name:
self.foot.append([self._model.get_geom_index(name), self.ground])
self.num_check = self.termination_contact.shape[0]
self.foot = None
for i in self._model.geom_names:
if i is not None and cfg.asset.foot_name in i:
if self.foot is None:
self.foot = np.array([[self._model.get_geom_index(i), self.ground]], dtype=np.uint32)
else:
self.foot = np.append(
self.foot,
np.array(
[[self._model.get_geom_index(i), self.ground]],
dtype=np.uint32,
),
axis=0,
)
self.foot_check_num = self.foot.shape[0]
self.foot_check = self.foot
self.termination_check = self.termination_contact
def apply_action(self, actions, state):
state.info["last_dof_vel"] = self.get_dof_vel(state.data)
state.info["last_actions"] = state.info["current_actions"]
state.info["current_actions"] = actions
state.data.actuator_ctrls = self._compute_torques(actions, state.data)
return state
def _compute_torques(self, actions, data):
# Compute torques from actions.
# pd controller
actions_scaled = actions * self.cfg.control_config.action_scale
torques = self.kps * (
actions_scaled + self.default_angles - self.get_dof_pos(data)
) - self.kds * self.get_dof_vel(data)
return torques
def get_local_linvel(self, data: mtx.SceneData) -> np.ndarray:
return self._model.get_sensor_value(self.cfg.sensor.local_linvel, data)
def get_gyro(self, data: mtx.SceneData) -> np.ndarray:
return self._model.get_sensor_value(self.cfg.sensor.gyro, data)
def update_state(self, state):
state = self.update_observation(state)
state = self.update_terminated(state)
state = self.update_reward(state)
return state
def _get_obs(self, data: mtx.SceneData, info: dict) -> np.ndarray:
linear_vel = self.get_local_linvel(data)
gyro = self.get_gyro(data)
pose = self._body.get_pose(data)
base_quat = pose[:, 3:7]
local_gravity = quat_rotate_inverse(base_quat, self.gravity_vec)
diff = self.get_dof_pos(data) - self.default_angles
noisy_linvel = linear_vel * self.cfg.normalization.lin_vel
noisy_gyro = gyro * self.cfg.normalization.ang_vel
noisy_joint_angle = diff * self.cfg.normalization.dof_pos
noisy_joint_vel = self.get_dof_vel(data) * self.cfg.normalization.dof_vel
command = info["commands"] * self.commands_scale
last_actions = info["current_actions"]
obs = np.hstack(
[
noisy_linvel,
noisy_gyro,
local_gravity,
noisy_joint_angle,
noisy_joint_vel,
last_actions,
command,
]
)
return obs
def update_observation(self, state: NpEnvState):
data = state.data
obs = self._get_obs(data, state.info)
cquerys = self._model.get_contact_query(data)
foot_contact = cquerys.is_colliding(self.foot_check)
state.info["contacts"] = foot_contact.reshape((self._num_envs, self.foot_check_num))
state.info["feet_air_time"] = self.update_feet_air_time(state.info)
return state.replace(obs=obs)
def update_terminated(self, state: NpEnvState) -> NpEnvState:
data = state.data
cquerys = self._model.get_contact_query(data)
termination_check = cquerys.is_colliding(self.termination_check)
termination_check.reshape((self._num_envs, self.num_check))
terminated = termination_check.any(axis=1)
return state.replace(
terminated=terminated,
)
def update_feet_air_time(self, info: dict):
feet_air_time = info["feet_air_time"]
feet_air_time += self.cfg.ctrl_dt
feet_air_time *= ~info["contacts"]
return feet_air_time
def resample_commands(self, num_envs: int):
commands = np.random.uniform(
low=self.cfg.commands.vel_limit[0],
high=self.cfg.commands.vel_limit[1],
size=(num_envs, 3),
)
return commands
def update_reward(self, state: NpEnvState) -> NpEnvState:
data = state.data
terminated = state.terminated
reward_dict = self._get_reward(data, state.info)
rewards = {k: v * self.cfg.reward_config.scales[k] for k, v in reward_dict.items()}
rwd = sum(rewards.values())
rwd = np.clip(rwd, 0.0, 10000.0)
if "termination" in self.cfg.reward_config.scales:
termination = self._reward_termination(terminated) * self.cfg.reward_config.scales["termination"]
rwd += termination
rwd = np.where(terminated, np.array(0.0), rwd)
return state.replace(reward=rwd)
def reset(self, data) -> tuple[np.ndarray, dict]:
num_reset = data.shape[0]
dof_pos = np.tile(self._init_dof_pos, (num_reset, 1))
dof_vel = np.tile(self._init_dof_vel, (num_reset, 1))
data.reset(self._model)
data.set_dof_vel(dof_vel)
data.set_dof_pos(dof_pos, self._model)
self._model.forward_kinematic(data)
info = {
"current_actions": np.zeros((num_reset, self._num_action), dtype=np.float32),
"last_actions": np.zeros((num_reset, self._num_action), dtype=np.float32),
"commands": self.resample_commands(num_reset),
"last_dof_vel": np.zeros((num_reset, self._num_action), dtype=np.float32),
"feet_air_time": np.zeros((num_reset, self.foot_check_num), dtype=np.float32),
"contacts": np.zeros((num_reset, self.foot_check_num), dtype=np.bool),
}
obs = self._get_obs(data, info)
return obs, info
def _get_reward(
self,
data: mtx.SceneData,
info: dict,
) -> dict[str, np.ndarray]:
commands = info["commands"]
return {
"lin_vel_z": self._reward_lin_vel_z(data),
"ang_vel_xy": self._reward_ang_vel_xy(data),
"orientation": self._reward_orientation(data),
"torques": self._reward_torques(data),
"dof_vel": self._reward_dof_vel(data),
"dof_acc": self._reward_dof_acc(data, info),
"action_rate": self._reward_action_rate(info),
"tracking_lin_vel": self._reward_tracking_lin_vel(data, commands),
"tracking_ang_vel": self._reward_tracking_ang_vel(data, commands),
"stand_still": self._reward_stand_still(data, commands),
"hip_pos": self._reward_hip_pos(data, commands),
"calf_pos": self._reward_calf_pos(data, commands),
"feet_air_time": self._reward_feet_air_time(commands, info),
}
# ------------ reward functions----------------
def _reward_lin_vel_z(self, data):
# Penalize z axis base linear velocity
return np.square(self.get_local_linvel(data)[:, 2])
def _reward_ang_vel_xy(self, data):
# Penalize xy axes base angular velocity
return np.sum(np.square(self.get_gyro(data)[:, :2]), axis=1)
def _reward_orientation(self, data):
# Penalize non flat base orientation
pose = self._body.get_pose(data)
base_quat = pose[:, 3:7]
gravity = quat_rotate_inverse(base_quat, self.gravity_vec)
return np.sum(np.square(gravity[:, :2]), axis=1)
def _reward_torques(self, data: mtx.SceneData):
# Penalize torques
return np.sum(np.square(data.actuator_ctrls), axis=1)
def _reward_dof_vel(self, data):
# Penalize dof velocities
return np.sum(np.square(self.get_dof_vel(data)), axis=1)
def _reward_dof_acc(self, data, info):
# Penalize dof accelerations
return np.sum(
np.square((info["last_dof_vel"] - self.get_dof_vel(data)) / self.cfg.ctrl_dt),
axis=1,
)
def _reward_action_rate(self, info: dict):
# Penalize changes in actions
action_diff = info["current_actions"] - info["last_actions"]
return np.sum(np.square(action_diff), axis=1)
def _reward_termination(self, done):
# Terminal reward / penalty
return done
def _reward_feet_air_time(self, commands: np.ndarray, info: dict):
# Reward long steps
feet_air_time = info["feet_air_time"]
first_contact = (feet_air_time > 0.0) * info["contacts"]
# reward only on first contact with the ground
rew_airTime = np.sum((feet_air_time - 0.5) * first_contact, axis=1)
# no reward for zero command
rew_airTime *= np.linalg.norm(commands[:, :2], axis=1) > 0.1
return rew_airTime
def _reward_tracking_lin_vel(self, data, commands: np.ndarray):
# Tracking of linear velocity commands (xy axes)
lin_vel_error = np.sum(np.square(commands[:, :2] - self.get_local_linvel(data)[:, :2]), axis=1)
return np.exp(-lin_vel_error / self.cfg.reward_config.tracking_sigma)
def _reward_tracking_ang_vel(self, data, commands: np.ndarray):
# Tracking of angular velocity commands (yaw)
ang_vel_error = np.square(commands[:, 2] - self.get_gyro(data)[:, 2])
return np.exp(-ang_vel_error / self.cfg.reward_config.tracking_sigma)
def _reward_stand_still(self, data, commands: np.ndarray):
# Penalize motion at zero commands
return np.sum(np.abs(self.get_dof_pos(data) - self.default_angles), axis=1) * (
np.linalg.norm(commands, axis=1) < 0.1
)
def _reward_hip_pos(self, data, commands: np.ndarray):
return (0.8 - np.abs(commands[:, 1])) * np.sum(
np.square(self.get_dof_pos(data)[:, self.hip_indices] - self.default_angles[self.hip_indices]),
axis=1,
)
def _reward_calf_pos(self, data, commands: np.ndarray):
return (0.8 - np.abs(commands[:, 1])) * np.sum(
np.square(self.get_dof_pos(data)[:, self.calf_indices] - self.default_angles[self.calf_indices]),
axis=1,
)

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ad3685e132454fd64e4ca3e5089f0d812fb53958e95c14913a22e3193e9e9ed8
size 1447984

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b6599b9dd2751ebd87449203223f5683a6c235268b76fcc443e00e0d974a06c0
size 1505284

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bbf7c0a441323088c59276dbec8a32e708a1c61f42d30a6f6775176366ea23f8
size 793584

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1a90b89de47a57cb12a2d703f6b008a6ace8a1890a381c27723296fd0d00b9d8
size 950834

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:051a57995dc6f892fc9bc53cd94e9a894f3dc209fd646e94d55d764150013970
size 5619284

View File

@@ -0,0 +1,238 @@
<mujoco model="go1">
<compiler angle="radian" autolimits="true"/>
<option timestep="0.005" integrator="Euler" iterations="60">
<flag eulerdamp="disable"/>
</option>
<custom>
<numeric data="30" name="max_contact_points"/>
<numeric data="12" name="max_geom_pairs"/>
</custom>
<default>
<default class="go1">
<geom condim="1"/>
<joint axis="0 1 0" armature="0.005" damping="0.5"/>
<!-- <position forcerange="-23.7 23.7" inheritrange="1" kp="35"/> -->
<default class="abduction">
<joint axis="1 0 0" range="-0.863 0.863" frictionloss="0.3"/>
</default>
<default class="hip">
<joint range="-0.686 4.501" frictionloss="0.3"/>
</default>
<default class="knee">
<joint range="-2.818 -0.888" frictionloss="1.0"/>
<position forcerange="-35.55 35.55"/>
</default>
<default class="visual">
<geom type="mesh" contype="0" conaffinity="0" group="2" material="dark"/>
</default>
<default class="collision">
<geom group="3" type="capsule"/>
<default class="hip_left1">
<geom size="0.046 0.02" pos="0 0.045 0" quat="1 1 0 0" type="cylinder"/>
</default>
<default class="hip_left2">
<geom size="0.031 0.02" pos="0 0.065 0" quat="1 1 0 0" type="cylinder"/>
</default>
<default class="hip_left3">
<geom size="0.046 0.02" quat="1 1 0 0" type="cylinder"/>
</default>
<default class="hip_right1">
<geom size="0.046 0.02" pos="0 -0.045 0" quat="1 1 0 0" type="cylinder"/>
</default>
<default class="hip_right2">
<geom size="0.031 0.02" pos="0 -0.065 0" quat="1 1 0 0" type="cylinder"/>
</default>
<default class="hip_right3">
<geom size="0.046 0.02" quat="1 1 0 0" type="cylinder"/>
</default>
<default class="thigh1">
<geom size="0.015" fromto="-0.02 0 0 -0.02 0 -0.16"/>
</default>
<default class="thigh2">
<geom size="0.015" fromto="0 0 0 -0.02 0 -0.1"/>
</default>
<default class="thigh3">
<geom size="0.015" fromto="-0.02 0 -0.16 0 0 -0.2"/>
</default>
<default class="calf1">
<geom size="0.01" fromto="0 0 0 0.02 0 -0.13"/>
</default>
<default class="calf2">
<geom size="0.01" fromto="0.02 0 -0.13 0 0 -0.2"/>
</default>
<default class="foot">
<geom type="sphere" size="0.023" pos="0 0 -0.213" solimp="0.9 .99 0.001" priority="10" condim="3"/>
</default>
</default>
</default>
</default>
<asset>
<material name="dark" rgba="0.2 0.2 0.2 1"/>
<mesh class="go1" file="./assets/trunk.stl"/>
<mesh class="go1" file="./assets/hip.stl"/>
<mesh class="go1" file="./assets/thigh_mirror.stl"/>
<mesh class="go1" file="./assets/calf.stl"/>
<mesh class="go1" file="./assets/thigh.stl"/>
</asset>
<worldbody>
<body name="trunk" pos="0 0 0.4" childclass="go1">
<camera name="track" pos="0.846 -1.3 0.316" xyaxes="0.866 0.500 0.000 -0.171 0.296 0.940" mode="trackcom"/>
<camera name="top" pos="-1 0 1" xyaxes="0 -1 0 0.7 0 0.7" mode="trackcom"/>
<camera name="side" pos="0 -2 1" xyaxes="1 0 0 0 1 2" mode="trackcom"/>
<!-- <camera name="back" pos="-2.4 0 0.8" target="trunk" mode="targetbody" trackposspeed="2" trackrotspeed="10"/> -->
<site name="head" pos="0.3 0 0" rgba="1 0 0 1" size="0.02" group="5"/>
<inertial pos="0.0223 0.002 -0.0005" quat="-0.00342088 0.705204 0.000106698 0.708996" mass="5.204" diaginertia="0.0716565 0.0630105 0.0168101"/>
<freejoint/>
<geom class="visual" mesh="trunk"/>
<geom name='trunk' class="collision" quat="1 0 0 0" pos="0.24 0 0" size="0.05 0.05 0.05" type="box"/>
<!-- <geom name='trunk1' class="collision" quat="1 0 1 0" pos="0 -0.04 0" size="0.058 0.125" type="cylinder"/> -->
<!-- <geom name='trunk2' class="collision" quat="1 0 1 0" pos="0 +0.04 0" size="0.058 0.125" type="cylinder"/> -->
<site name="imu" pos="-0.01592 -0.06659 -0.00617" group="5"/>
<body name="FR_hip" pos="0.1881 -0.04675 0">
<inertial pos="-0.0049166 0.00762615 -8.865e-05" quat="0.507341 0.514169 0.495027 0.482891" mass="0.68" diaginertia="0.000734064 0.000468438 0.000398719"/>
<joint class="abduction" name="FR_hip_joint"/>
<geom class="visual" mesh="hip" quat="1 0 0 0"/>
<!-- <geom name="fr_hip" class="hip_right1"/> -->
<body name="FR_thigh" pos="0 -0.08 0">
<inertial pos="-0.00304722 0.019315 -0.0305004" quat="0.65243 -0.0272313 0.0775126 0.753383" mass="1.009" diaginertia="0.00478717 0.00460903 0.000709268"/>
<joint class="hip" name="FR_thigh_joint"/>
<geom class="visual" mesh="thigh_mirror"/>
<!-- <geom name="fr_thigh1" class="thigh1"/> -->
<!-- <geom name="fr_thigh2" class="thigh2"/> -->
<!-- <geom name="fr_thigh3" class="thigh3"/> -->
<body name="FR_calf" pos="0 0 -0.213">
<inertial pos="0.00429862 0.000976676 -0.146197" quat="0.691246 0.00357467 0.00511118 0.722592" mass="0.195862" diaginertia="0.00149767 0.00148468 3.58427e-05"/>
<joint class="knee" name="FR_calf_joint"/>
<geom class="visual" mesh="calf"/>
<!-- <geom name="fr_calf1" class="calf1"/> -->
<!-- <geom name="fr_calf2" class="calf2"/> -->
<geom name="FR_foot" class="foot"/>
<site name="FR" pos="0 0 -0.213" type="sphere" size="0.023" group="5"/>
</body>
</body>
</body>
<body name="FL_hip" pos="0.1881 0.04675 0">
<inertial pos="-0.0049166 -0.00762615 -8.865e-05" quat="0.482891 0.495027 0.514169 0.507341" mass="0.68" diaginertia="0.000734064 0.000468438 0.000398719"/>
<joint class="abduction" name="FL_hip_joint"/>
<geom class="visual" mesh="hip"/>
<!-- <geom name="fl_hip" class="hip_left1"/> -->
<body name="FL_thigh" pos="0 0.08 0">
<inertial pos="-0.00304722 -0.019315 -0.0305004" quat="0.753383 0.0775126 -0.0272313 0.65243" mass="1.009" diaginertia="0.00478717 0.00460903 0.000709268"/>
<joint class="hip" name="FL_thigh_joint"/>
<geom class="visual" mesh="thigh"/>
<!-- <geom name="fl_thigh1" class="thigh1"/> -->
<!-- <geom name="fl_thigh2" class="thigh2"/> -->
<!-- <geom name="fl_thigh3" class="thigh3"/> -->
<body name="FL_calf" pos="0 0 -0.213">
<inertial pos="0.00429862 0.000976676 -0.146197" quat="0.691246 0.00357467 0.00511118 0.722592" mass="0.195862" diaginertia="0.00149767 0.00148468 3.58427e-05"/>
<joint class="knee" name="FL_calf_joint"/>
<geom class="visual" mesh="calf"/>
<!-- <geom name="fl_calf1" class="calf1"/> -->
<!-- <geom name="fl_calf2" class="calf2"/> -->
<geom name="FL_foot" class="foot"/>
<site name="FL" pos="0 0 -0.213" type="sphere" size="0.023" group="5"/>
</body>
</body>
</body>
<body name="RR_hip" pos="-0.1881 -0.04675 0">
<inertial pos="0.0049166 0.00762615 -8.865e-05" quat="0.495027 0.482891 0.507341 0.514169" mass="0.68" diaginertia="0.000734064 0.000468438 0.000398719"/>
<joint class="abduction" name="RR_hip_joint"/>
<geom class="visual" quat="0 0 0 -1" mesh="hip"/>
<!-- <geom name="rr_hip" class="hip_right1"/> -->
<body name="RR_thigh" pos="0 -0.08 0">
<inertial pos="-0.00304722 0.019315 -0.0305004" quat="0.65243 -0.0272313 0.0775126 0.753383" mass="1.009" diaginertia="0.00478717 0.00460903 0.000709268"/>
<joint class="hip" name="RR_thigh_joint"/>
<geom class="visual" mesh="thigh_mirror"/>
<!-- <geom name="rr_thigh1" class="thigh1"/>
<geom name="rr_thigh2" class="thigh2"/>
<geom name="rr_thigh3" class="thigh3"/> -->
<body name="RR_calf" pos="0 0 -0.213">
<inertial pos="0.00429862 0.000976676 -0.146197" quat="0.691246 0.00357467 0.00511118 0.722592" mass="0.195862" diaginertia="0.00149767 0.00148468 3.58427e-05"/>
<joint class="knee" name="RR_calf_joint"/>
<geom class="visual" mesh="calf"/>
<!-- <geom name="rr_calf1" class="calf1"/>
<geom name="rr_calf2" class="calf2"/> -->
<geom name="RR_foot" class="foot"/>
<site name="RR" pos="0 0 -0.213" type="sphere" size="0.023" group="5"/>
</body>
</body>
</body>
<body name="RL_hip" pos="-0.1881 0.04675 0">
<inertial pos="0.0049166 -0.00762615 -8.865e-05" quat="0.514169 0.507341 0.482891 0.495027" mass="0.68" diaginertia="0.000734064 0.000468438 0.000398719"/>
<joint class="abduction" name="RL_hip_joint"/>
<geom class="visual" quat="0 0 1 0" mesh="hip"/>
<!-- <geom name="rl_hip" class="hip_left1"/> -->
<body name="RL_thigh" pos="0 0.08 0">
<inertial pos="-0.00304722 -0.019315 -0.0305004" quat="0.753383 0.0775126 -0.0272313 0.65243" mass="1.009" diaginertia="0.00478717 0.00460903 0.000709268"/>
<joint class="hip" name="RL_thigh_joint"/>
<geom class="visual" mesh="thigh"/>
<!-- <geom name="rl_thigh1" class="thigh1"/>
<geom name="rl_thigh2" class="thigh2"/>
<geom name="rl_thigh3" class="thigh3"/> -->
<body name="RL_calf" pos="0 0 -0.213">
<inertial pos="0.00429862 0.000976676 -0.146197" quat="0.691246 0.00357467 0.00511118 0.722592" mass="0.195862" diaginertia="0.00149767 0.00148468 3.58427e-05"/>
<joint class="knee" name="RL_calf_joint"/>
<geom class="visual" mesh="calf"/>
<!-- <geom name="rl_calf1" class="calf1"/>
<geom name="rl_calf2" class="calf2"/> -->
<geom name="RL_foot" class="foot"/>
<site name="RL" pos="0 0 -0.213" type="sphere" size="0.023" group="5"/>
</body>
</body>
</body>
</body>
</worldbody>
<actuator>
<motor class="abduction" name="FR_hip" joint="FR_hip_joint" ctrllimited="true" ctrlrange="-23.7 23.7"/>
<motor class="hip" name="FR_thigh" joint="FR_thigh_joint" ctrllimited="true" ctrlrange="-23.7 23.7"/>
<motor class="knee" name="FR_calf" joint="FR_calf_joint" ctrllimited="true" ctrlrange="-23.7 23.7"/>
<motor class="abduction" name="FL_hip" joint="FL_hip_joint" ctrllimited="true" ctrlrange="-23.7 23.7"/>
<motor class="hip" name="FL_thigh" joint="FL_thigh_joint" ctrllimited="true" ctrlrange="-23.7 23.7"/>
<motor class="knee" name="FL_calf" joint="FL_calf_joint" ctrllimited="true" ctrlrange="-23.7 23.7"/>
<motor class="abduction" name="RR_hip" joint="RR_hip_joint" ctrllimited="true" ctrlrange="-23.7 23.7"/>
<motor class="hip" name="RR_thigh" joint="RR_thigh_joint" ctrllimited="true" ctrlrange="-23.7 23.7"/>
<motor class="knee" name="RR_calf" joint="RR_calf_joint" ctrllimited="true" ctrlrange="-23.7 23.7"/>
<motor class="abduction" name="RL_hip" joint="RL_hip_joint" ctrllimited="true" ctrlrange="-23.7 23.7"/>
<motor class="hip" name="RL_thigh" joint="RL_thigh_joint" ctrllimited="true" ctrlrange="-23.7 23.7"/>
<motor class="knee" name="RL_calf" joint="RL_calf_joint" ctrllimited="true" ctrlrange="-23.7 23.7"/>
<!-- <position class="abduction" name="FR_hip" joint="FR_hip_joint" />
<position class="hip" name="FR_thigh" joint="FR_thigh_joint" />
<position class="knee" name="FR_calf" joint="FR_calf_joint" />
<position class="abduction" name="FL_hip" joint="FL_hip_joint" />
<position class="hip" name="FL_thigh" joint="FL_thigh_joint" />
<position class="knee" name="FL_calf" joint="FL_calf_joint" />
<position class="abduction" name="RR_hip" joint="RR_hip_joint" />
<position class="hip" name="RR_thigh" joint="RR_thigh_joint" />
<position class="knee" name="RR_calf" joint="RR_calf_joint" />
<position class="abduction" name="RL_hip" joint="RL_hip_joint" />
<position class="hip" name="RL_thigh" joint="RL_thigh_joint" />
<position class="knee" name="RL_calf" joint="RL_calf_joint" /> -->
</actuator>
<sensor>
<gyro site="imu" name="gyro"/>
<velocimeter site="imu" name="local_linvel"/>
<framepos objtype="site" objname="imu" name="position"/>
<!-- <framezaxis objtype="site" objname="imu" name="upvector"/>
<framexaxis objtype="site" objname="imu" name="forwardvector"/> -->
<framelinvel objtype="site" objname="imu" name="global_linvel"/>
<frameangvel objtype="site" objname="imu" name="global_angvel"/>
<framequat objtype="site" objname="imu" name="orientation"/>
<framelinvel objtype="site" objname="FR" name="FR_global_linvel"/>
<framelinvel objtype="site" objname="FL" name="FL_global_linvel"/>
<framelinvel objtype="site" objname="RR" name="RR_global_linvel"/>
<framelinvel objtype="site" objname="RL" name="RL_global_linvel"/>
<framepos objtype="site" objname="FR" name="FR_pos" reftype="site" refname="imu"/>
<framepos objtype="site" objname="FL" name="FL_pos" reftype="site" refname="imu"/>
<framepos objtype="site" objname="RR" name="RR_pos" reftype="site" refname="imu"/>
<framepos objtype="site" objname="RL" name="RL_pos" reftype="site" refname="imu"/>
<framepos objtype="site" objname="head" name="head_pos"/>
</sensor>
</mujoco>

View File

@@ -0,0 +1,58 @@
<mujoco model="go1 feetonly flat terrain scene">
<include file="go1_motor_actuator.xml"/>
<statistic center="0 0 0.1" extent="0.8" meansize="0.04"/>
<visual>
<headlight diffuse="0.6 0.6 0.6" ambient="0.3 0.3 0.3" specular="0 0 0"/>
<rgba haze="0.15 0.25 0.35 1"/>
<global azimuth="120" elevation="-20"/>
<map force="0.01"/>
<scale forcewidth="0.3" contactwidth="0.5" contactheight="0.2"/>
<quality shadowsize="8192"/>
</visual>
<asset>
<texture type="skybox" builtin="gradient" rgb1="0.4314 0.5294 0.6431" rgb2="0 0 0" width="512" height="512"/>
<texture type="2d" name="groundplane" builtin="checker" mark="edge" rgb1="0.4314 0.5294 0.6431" rgb2="0.8157 0.8549 0.9059"
markrgb="0.8 0.8 0.8" width="300" height="300"/>
<material name="groundplane" texture="groundplane" texuniform="true" texrepeat="1 1" reflectance="0.2"/>
</asset>
<worldbody>
<light pos="0 0 1.5" dir="0 0 -1" directional="true"/>
<geom name="floor" size="0 0 0.01" type="plane" material="groundplane" contype="1" conaffinity="0" priority="1"
friction="0.6" condim="3"/>
</worldbody>
<keyframe>
<key name="home" qpos="
0 0 0.278
1 0 0 0
0.1 0.9 -1.8
-0.1 0.9 -1.8
0.1 0.9 -1.8
-0.1 0.9 -1.8"
ctrl="0.1 0.9 -1.8 -0.1 0.9 -1.8 0.1 0.9 -1.8 -0.1 0.9 -1.8"/>
<key name="home_higher" qpos="0 0 0.31 1 0 0 0 0 0.82 -1.63 0 0.82 -1.63 0 0.82 -1.63 0 0.82 -1.63"
ctrl="0 0.82 -1.63 0 0.82 -1.63 0 0.82 -1.63 0 0.82 -1.63"/>
<key
name="footstand"
qpos='
0 0 0.54
0.8 0 -0.8 0
0 0.82 -1.6 0 0.82 -1.68 0 1.82 -1.16 0.0 1.82 -1.16'
ctrl='0 0.82 -1.6 0 0.82 -1.68 0 1.82 -1.16 0.0 1.82 -1.16'/>
<key name="handstand"
qpos="0 0 0.54
0.8 0 0.8 0
0 -0.686 -1.16
0 -0.686 -1.16
0 1.7 -1.853
0 1.7 -1.853"
ctrl="0 -0.686 -1.16 0 -0.686 -1.16 0 1.7 -1.853 0 1.7 -1.853"/>
<key name="pre_recovery"
qpos="-0.0318481 -0.000215369 0.0579031 1 -2.70738e-05 6.06169e-05 0.000231261 -0.352275 1.18554 -2.80738 0.360892 1.1806 -2.80281 -0.381197 1.16812 -2.79123 0.391054 1.1622 -2.78576"
ctrl="-0.352275 1.18554 -2.80738 0.360892 1.1806 -2.80281 -0.381197 1.16812 -2.79123 0.391054 1.1622 -2.78576"/>
</keyframe>
</mujoco>

View File

@@ -0,0 +1,199 @@
# Copyright (C) 2020-2025 Motphys Technology Co., Ltd. All Rights Reserved.
#
# 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.
# ==============================================================================
import abc
import dataclasses
from dataclasses import dataclass
import motrixsim as mtx
import numpy as np
from motrix_envs.base import ABEnv, EnvCfg
@dataclass
class NpEnvState:
data: mtx.SceneData
obs: np.ndarray
reward: np.ndarray
terminated: np.ndarray
truncated: np.ndarray
info: dict
@property
def done(self) -> np.ndarray:
"""
Check if the environment is done.
"""
return np.logical_or(self.terminated, self.truncated)
def replace(self, **updates) -> "NpEnvState":
return dataclasses.replace(self, **updates)
def validate(self):
num_envs = self.data.shape[0]
assert self.reward.shape == (num_envs,), self.reward.shape
assert self.terminated.shape == (num_envs,), self.terminated.shape
assert self.truncated.shape == (num_envs,), self.truncated.shape
class NpEnv(ABEnv):
_model: mtx.SceneModel
_cfg: EnvCfg
_state: NpEnvState = None
def __init__(self, cfg: EnvCfg, num_envs: int = 1):
self._cfg = cfg
self._num_envs = num_envs
self._model = mtx.load_model(cfg.model_file)
self._model.options.timestep = cfg.sim_dt
@property
def model(self) -> mtx.SceneModel:
"""
Get the scene model
"""
return self._model
@property
def state(self) -> NpEnvState:
"""
Get the current environment state
"""
return self._state
@property
def cfg(self) -> EnvCfg:
"""
Get the environment configuration
"""
return self._cfg
@property
def num_envs(self) -> int:
return self._num_envs
def init_state(self) -> NpEnvState:
"""
Create a new environment state
"""
obs = np.zeros((self._num_envs, self.observation_space.shape[0]), dtype=np.float32)
reward = np.zeros((self._num_envs,), dtype=np.float32)
terminated = np.ones((self._num_envs,), dtype=bool)
truncated = np.zeros((self._num_envs,), dtype=bool)
info = {"steps": np.zeros((self._num_envs,), dtype=np.uint64)}
data = mtx.SceneData(self._model, batch=[self._num_envs])
self._state = NpEnvState(data, obs, reward, terminated, truncated, info)
self._reset_done_envs()
self._state.validate()
return self._state
def _reset_done_envs(self):
"""
Reset the environments that are done
"""
state = self._state
done = state.done
assert done.shape == (self._num_envs,)
if not np.any(done):
return
np.putmask(state.info["steps"], done, 0)
data = state.data[done]
obs, info1 = self.reset(data)
state.obs[done] = obs
if info1:
def replace_dict_values(dst, new_values, mask):
for key, value in new_values.items():
if key not in dst:
dst[key] = value
else:
if isinstance(value, np.ndarray):
dst[key][mask] = value
elif isinstance(value, dict):
assert isinstance(dst[key], dict)
replace_dict_values(dst[key], value, mask)
replace_dict_values(state.info, info1, done)
def _update_truncate(self):
"""
Truncate the environments that have reached max episode length
"""
if not self._cfg.max_episode_steps:
return
self._state.truncated = self._state.info["steps"] >= self._cfg.max_episode_steps
@abc.abstractmethod
def apply_action(self, actions: np.ndarray, state: NpEnvState) -> NpEnvState:
"""
Apply the action to the environment
Args:
actions (np.ndarray): The actions to apply
state (NpEnvState): The environment state to apply the actions.
"""
@abc.abstractmethod
def update_state(self, state: NpEnvState) -> NpEnvState:
"""
Update the environment state after physics step
Args:
state (NpEnvState): The environment state to update
"""
@abc.abstractmethod
def reset(
self,
data: mtx.SceneData,
done: np.ndarray = None,
) -> tuple[np.ndarray, dict]:
"""
Reset the environment for the done envs
Args:
data (mtx.SceneData): The scene data to reset
done (Optional[np.ndarray]): A boolean array indicating which envs to reset. If None, reset all envs.
Returns:
tuple[np.ndarray, dict]: The initial observations and info after reset
"""
pass
def physics_step(self):
for _ in range(self._cfg.sim_substeps):
self._model.step(self._state.data)
def _prev_physics_step(self):
state = self._state
state.reward.fill(0.0)
state.terminated.fill(False)
state.truncated.fill(False)
def step(self, actions: np.ndarray) -> NpEnvState:
if self._state is None:
self.init_state()
self._prev_physics_step()
self._state = self.apply_action(actions, self._state)
assert self._state is not None, "apply_action must return a valid NpEnvState"
self.physics_step()
self._state = self.update_state(self._state)
self._state.info["steps"] += 1
self._update_truncate()
self._reset_done_envs()
return self._state

View File

@@ -0,0 +1,64 @@
# Copyright (C) 2020-2025 Motphys Technology Co., Ltd. All Rights Reserved.
#
# 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.
# ==============================================================================
import numpy as np
from motrixsim.render import RenderApp, RenderSettings
from motrix_envs.np.env import NpEnv
class NpRenderer:
"""
The renderer for Np sim environments.
"""
_env: NpEnv
def __init__(self, env: NpEnv):
num_envs = env.num_envs
num_envs = 1 if num_envs is None else num_envs
spacing = 1.0
cols = int(np.ceil(np.sqrt(num_envs)))
offsets = []
for i in range(num_envs):
row = i // cols
col = i % cols
x = col * spacing
y = row * spacing
z = 0.0
offsets.append([x, y, z])
self._env = env
self._render = RenderApp()
settings = RenderSettings.performance()
settings.enable_shadow = False # disable shadow for better performance
self._render.launch(
env.model,
batch=num_envs,
render_offset=offsets,
render_settings=settings,
)
self._sync_render_data = True
self._render.system_camera.active = self._sync_render_data
def render(self) -> None:
"""
render the env
"""
self._render.sync(data=self._env.state.data if self._sync_render_data else None)
if self._render.input.is_key_just_pressed("space"):
self._sync_render_data = not self._sync_render_data
self._render.system_camera.active = self._sync_render_data

View File

@@ -0,0 +1,83 @@
# Copyright (C) 2020-2025 Motphys Technology Co., Ltd. All Rights Reserved.
#
# 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.
# ==============================================================================
import numpy as np
_DEFAULT_VALUE_AT_MARGIN = 0.1
def _sigmoids(x, value_at_1, sigmoid):
if sigmoid in ("cosine", "linear", "quadratic"):
if not 0 <= value_at_1 < 1:
raise ValueError(f"`value_at_1` must be nonnegative and smaller than 1, got {value_at_1}.")
else:
if not 0 < value_at_1 < 1:
raise ValueError(f"`value_at_1` must be strictly between 0 and 1, got {value_at_1}.")
if sigmoid == "gaussian":
scale = np.sqrt(-2 * np.log(value_at_1))
return np.exp(-0.5 * (x * scale) ** 2)
elif sigmoid == "hyperbolic":
scale = np.arccosh(1 / value_at_1)
return 1 / np.cosh(x * scale)
elif sigmoid == "long_tail":
scale = np.sqrt(1 / value_at_1 - 1)
return 1 / ((x * scale) ** 2 + 1)
elif sigmoid == "reciprocal":
scale = 1 / value_at_1 - 1
return 1 / (abs(x) * scale + 1)
elif sigmoid == "linear":
scale = 1 - value_at_1
scaled_x = x * scale
return np.where(abs(scaled_x) < 1, 1 - scaled_x, 0.0)
elif sigmoid == "quadratic":
scale = np.sqrt(1 - value_at_1)
scaled_x = x * scale
return np.where(abs(scaled_x) < 1, 1 - scaled_x**2, 0.0)
elif sigmoid == "tanh_squared":
scale = np.arctanh(np.sqrt(1 - value_at_1))
return 1 - np.tanh(x * scale) ** 2
else:
raise ValueError(f"Unknown sigmoid type {sigmoid!r}.")
def tolerance(
x: np.ndarray,
bounds: tuple[float, float] = (0.0, 0.0),
margin: float = 0.0,
sigmoid: str = "gaussian",
value_at_margin: float = _DEFAULT_VALUE_AT_MARGIN,
) -> np.ndarray:
lower, upper = bounds
if lower > upper:
raise ValueError("lower bound must be less than upper bound")
if margin < 0:
raise ValueError("margin must be non-negative")
in_bounds = np.logical_and(lower <= x, x <= upper)
if margin == 0:
value = np.where(in_bounds, 1.0, 0.0)
else:
d = np.where(x < lower, lower - x, x - upper) / margin
value = np.where(in_bounds, 1.0, _sigmoids(d, value_at_margin, sigmoid))
return value

View File

@@ -0,0 +1,171 @@
# Copyright (C) 2020-2025 Motphys Technology Co., Ltd. All Rights Reserved.
#
# 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.
# ==============================================================================
from dataclasses import dataclass, field
from typing import Any, Callable, Dict, Optional, Type, TypeVar
from motrix_envs.base import ABEnv, EnvCfg
TEnvCfg = TypeVar("TEnvCfg", bound=EnvCfg)
@dataclass
class EnvMeta:
env_cfg_cls: Type[EnvCfg]
env_cls_dict: Dict[str, Type[ABEnv]] = field(default_factory=dict)
def available_sim_backend(self) -> Optional[str]:
"""Return the first available simulation backend."""
return next(iter(self.env_cls_dict), None)
def support_sim_backend(self, sim_backend: str) -> bool:
"""Check if the environment supports a specific simulation backend."""
return sim_backend in self.env_cls_dict
_envs: Dict[str, EnvMeta] = {}
def contains(name: str) -> bool:
"""Check if an environment configuration is registered."""
return name in _envs
def register_env_config(name: str, env_cfg_cls: Type[EnvCfg]):
"""Register an environment configuration class with a name."""
if name in _envs.keys():
raise ValueError(f"Environment '{name}' is already registered.")
_envs[name] = EnvMeta(env_cfg_cls=env_cfg_cls)
def envcfg(name: str) -> Callable[[Type[TEnvCfg]], Type[TEnvCfg]]:
"""
Decorator to register an environment configuration class with a name.
Usage:
@register_env_config_decorator("my-env")
@dataclass
class MyEnvCfg(EnvCfg):
...
"""
def decorator(cls: Type[TEnvCfg]) -> Type[TEnvCfg]:
register_env_config(name, cls)
return cls
return decorator
def register_env(name: str, env_cls: Type[ABEnv], sim_backend: str):
"""Register an environment class with a name and simulation backend."""
if sim_backend not in ["np"]:
raise ValueError(f"Unsupported simulation backend: {sim_backend}. Only 'np' is supported yet.")
if name not in _envs:
raise ValueError(f"Environment '{name}' is not registered. Please register the config first.")
if sim_backend in _envs[name].env_cls_dict:
raise ValueError(f"Environment '{name}' with sim backend '{sim_backend}' is already registered.")
_envs[name].env_cls_dict[sim_backend] = env_cls
def env(name: str, sim_backend: str) -> Callable[[Type[ABEnv]], Type[ABEnv]]:
"""
Decorator to register an environment class with a name and simulation backend.
Usage:
@register_env_decorator("my-env", "np")
class MyEnv(ABEnv):
...
"""
def decorator(cls: Type[ABEnv]) -> Type[ABEnv]:
register_env(name, cls, sim_backend)
return cls
return decorator
def find_available_sim_backend(env_name: str) -> str:
"""Find the first available simulation backend for an environment."""
if env_name not in _envs:
raise ValueError(f"Environment '{env_name}' is not registered.")
meta: EnvMeta = _envs[env_name]
backend = meta.available_sim_backend()
if backend is None:
raise ValueError(f"Environment '{env_name}' does not support any simulation backend.")
return backend
def make(
name: str,
sim_backend: Optional[str] = None,
env_cfg_override: Optional[Dict[str, Any]] = None,
num_envs: int = 1,
) -> ABEnv:
"""
Create an environment instance by name.
Args:
name: Environment name
sim_backend: Simulation backend ("np"). If None, uses first available.
env_cfg_override: Dictionary of config overrides
num_envs: Number of environments to create
Returns:
Environment instance
"""
if name not in _envs:
raise ValueError(f"Environment '{name}' is not registered.")
meta: EnvMeta = _envs[name]
# Create environment config
env_cfg = meta.env_cfg_cls()
if env_cfg_override is not None:
for key, value in env_cfg_override.items():
if hasattr(env_cfg, key):
setattr(env_cfg, key, value)
else:
raise ValueError(f"Config class '{env_cfg.__class__.__name__}' has no attribute '{key}'")
# Validate config
env_cfg.validate()
# Select simulation backend
if sim_backend is None:
sim_backend = meta.available_sim_backend()
if sim_backend is None:
raise ValueError(f"Environment '{name}' does not support any simulation backend.")
if not meta.support_sim_backend(sim_backend):
raise ValueError(f"Environment '{name}' does not support simulation backend '{sim_backend}'.")
# Create environment instance
env_cls = meta.env_cls_dict[sim_backend]
return env_cls(env_cfg, num_envs=num_envs)
def list_registered_envs() -> Dict[str, Dict[str, Any]]:
"""List all registered environments with their available backends."""
result = {}
for name, meta in _envs.items():
result[name] = {
"config_class": meta.env_cfg_cls.__name__,
"available_backends": list(meta.env_cls_dict.keys()),
}
return result

3
motrix_rl/README.md Normal file
View File

@@ -0,0 +1,3 @@
# MotrixLab
集成 rl 框架,并针对 motrix_envs 里的各种环境提供默认的训练参数。 预期会支持不同的 rl 框架的不同 rl 算法,但目前只支持 了 skrl 框架的 ppo 算法

30
motrix_rl/pyproject.toml Normal file
View File

@@ -0,0 +1,30 @@
[build-system]
requires = ["uv_build>=0.9.6,<0.10.0"]
build-backend = "uv_build"
[project]
name = "motrix-rl"
version = "0.0.1"
description = "Reinforcement learning training framework based on SKRL with multi-backend unified training interface."
authors = [{ name = "Motphys", email = "developers@motphys.com" }]
requires-python = "==3.10.*"
readme = "README.md"
license = "Apache-2.0"
dependencies = ["gymnasium===1.1.1", "python-abc>=0.2.0", "motrix-envs"]
[project.optional-dependencies]
skrl-jax = [
"skrl===1.4.3; sys_platform == 'linux'",
"jax[cuda12]==0.4.34; sys_platform == 'linux'",
"flax===0.10.4; sys_platform == 'linux'",
]
skrl-torch = [
"skrl===1.4.3",
"torch===2.7.0+cu128",
"torchvision===v0.22.0+cu128",
"torchaudio===2.7.0+cu128",
]
test = ["pytest~=8.3.3"]
[tool.uv.sources]
motrix-envs = { workspace = true }

View File

@@ -0,0 +1,16 @@
# Copyright (C) 2020-2025 Motphys Technology Co., Ltd. All Rights Reserved.
#
# 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.
# ==============================================================================
from . import cfgs, skrl # noqa: F401

View File

@@ -0,0 +1,42 @@
# Copyright (C) 2020-2025 Motphys Technology Co., Ltd. All Rights Reserved.
#
# 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.
# ==============================================================================
import dataclasses
from typing import Optional
@dataclasses.dataclass
class BaseRLCfg:
"""
Config for the reinforcement learning algorithm
"""
# Basic training parameters
seed: Optional[int] = None
num_envs: int = 2048
play_num_envs: int = 16
max_env_steps: int = 20480000
check_point_interval: int = 1000
def replace(self, **updates) -> "BaseRLCfg":
return dataclasses.replace(self, **updates)
@property
def max_batch_env_steps(self) -> int:
"""
The max batched environment steps for the RL algorithm.
"""
n = int(self.max_env_steps / self.num_envs)
return (int)(n / self.check_point_interval) * self.check_point_interval

View File

@@ -0,0 +1,98 @@
# Copyright (C) 2020-2025 Motphys Technology Co., Ltd. All Rights Reserved.
#
# 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.
# ==============================================================================
from dataclasses import dataclass
from motrix_rl.registry import rlcfg
from motrix_rl.skrl.cfg import PPOCfg
class basic:
@rlcfg("cartpole")
@dataclass
class CartPolePPO(PPOCfg):
max_env_steps: int = 10_000_000
check_point_interval: int = 500
# Override PPO configuration
policy_hidden_layer_sizes: tuple[int, ...] = (32, 32)
value_hidden_layer_sizes: tuple[int, ...] = (32, 32)
rollouts: int = 32
learning_epochs: int = 5
mini_batches: int = 4
@rlcfg("dm-walker", backend="jax")
@rlcfg("dm-stander", backend="jax")
@rlcfg("dm-runner", backend="jax")
@dataclass
class WalkerPPO(PPOCfg):
seed: int = 42
max_env_steps: int = 1024 * 40000
num_envs: int = 2048
# Override PPO configuration
learning_rate: float = 2e-4
rollouts: int = 24
learning_epochs: int = 4
mini_batches: int = 4
@rlcfg("dm-stander", backend="torch")
@rlcfg("dm-walker", backend="torch")
@dataclass
class WalkerPPOTorch(PPOCfg):
seed: int = 42
max_env_steps: int = 1024 * 40000
num_envs: int = 2048
# Override PPO configuration
learning_rate: float = 2e-4
rollouts: int = 24
learning_epochs: int = 4
mini_batches: int = 32
@rlcfg("dm-runner", backend="torch")
@dataclass
class RunnerPPOTorch(PPOCfg):
seed: int = 42
max_env_steps: int = 1024 * 40000
num_envs: int = 2048
# Override PPO configuration
learning_rate: float = 2e-4
rollouts: int = 24
learning_epochs: int = 2
mini_batches: int = 32
class locomotion:
@rlcfg("go1-flat-terrain-walk")
@dataclass
class Go1WalkPPO(PPOCfg):
"""
Go1 Walk RL config
"""
seed: int = 42
share_policy_value_features: bool = False
max_env_steps: int = 1024 * 60000
num_envs: int = 2048
# Override PPO configuration
rollouts: int = 24
policy_hidden_layer_sizes: tuple[int, ...] = (256, 128, 64)
value_hidden_layer_sizes: tuple[int, ...] = (256, 128, 64)
learning_epochs: int = 5
mini_batches: int = 3
learning_rate: float = 3e-4

View File

@@ -0,0 +1,114 @@
# Copyright (C) 2020-2025 Motphys Technology Co., Ltd. All Rights Reserved.
#
# 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.
# ==============================================================================
import logging
from dataclasses import dataclass, field
from typing import Callable, Type, TypeVar
from motrix_envs import registry as env_registry
from motrix_rl.base import BaseRLCfg
logger = logging.getLogger(__name__)
TRLCfg = TypeVar("TRLCfg", bound=BaseRLCfg)
@dataclass
class EnvRlCfgs:
cfgs: dict[str, dict[str, Type[BaseRLCfg]]] = field(default_factory=dict)
"""
The RL configuration classes available for this environment.
Structure: {rl_framework: {backend: config_class}}
Example: {"skrl": {"jax": JaxConfig, "torch": TorchConfig}}
"""
# RL configuration registry. map from env name to EnvMeta
_rlcfgs: dict[str, EnvRlCfgs] = {}
def _register_rlcfg(env_name: str, rllib: str, backend: str, train_cfg_cls: Type[BaseRLCfg]):
"""
Register a training configuration class for an environment, reinforcement learning framework, and backend.
Args:
env_name: Environment name
rllib: RL framework name (e.g., "skrl")
backend: Backend name (e.g., "jax", "torch")
train_cfg_cls: Configuration class
"""
if not env_registry.contains(env_name):
raise ValueError(f"Environment '{env_name}' is not registered in env_registry.")
logger.info(f"Registering RL config for env '{env_name}', RL framework '{rllib}', and backend '{backend}'")
if env_name not in _rlcfgs:
_rlcfgs[env_name] = EnvRlCfgs()
if rllib not in _rlcfgs[env_name].cfgs:
_rlcfgs[env_name].cfgs[rllib] = {}
_rlcfgs[env_name].cfgs[rllib][backend] = train_cfg_cls
def rlcfg(env_name: str, backend: str = None) -> Callable[[Type[TRLCfg]], Type[TRLCfg]]:
"""
Decorator to register a training configuration class for an environment, RL framework, and backend.
Args:
env_name: Environment name
backend: Backend name (e.g., "jax", "torch"). If None, registers for all backends.
"""
def decorator(cls: Type[TRLCfg]) -> Type[TRLCfg]:
backends = ["jax", "torch"] if backend is None else [backend]
for b in backends:
_register_rlcfg(env_name, "skrl", b, cls)
return cls
return decorator
def default_rl_cfg(env_name: str, rllib: str, backend: str) -> BaseRLCfg:
"""
Get the default training configuration for an environment, reinforcement learning framework, and backend.
Args:
env_name: Environment name
rllib: RL framework name (e.g., "skrl")
backend: Backend name (e.g., "jax", "torch")
Returns:
The configuration class instance. Will use backend-specific config if available,
otherwise falls back to universal config (backend=None).
"""
if env_name not in _rlcfgs:
raise ValueError(f"Environment '{env_name}' is not registered.")
meta: EnvRlCfgs = _rlcfgs.get(env_name)
if rllib not in meta.cfgs:
raise ValueError(f"RL framework '{rllib}' is not supported for environment '{env_name}'.")
framework_configs = meta.cfgs[rllib]
# Try to get backend-specific config first
if backend in framework_configs:
return framework_configs[backend]()
# Fall back to universal config (backend=None) if backend-specific one is not found
if None in framework_configs:
return framework_configs[None]()
# If no universal config exists, raise an error
raise ValueError(
f"No configuration found for environment '{env_name}', RL framework '{rllib}', backend '{backend}', \
and no universal configuration available."
)

View File

@@ -0,0 +1,21 @@
# Copyright (C) 2020-2025 Motphys Technology Co., Ltd. All Rights Reserved.
#
# 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.
# ==============================================================================
LOG_DIR_PREFIX = "runs"
def get_log_dir(env_name: str) -> str:
"""Get the log directory for the given environment name."""
return f"{LOG_DIR_PREFIX}/{env_name}"

View File

@@ -0,0 +1,73 @@
# Copyright (C) 2020-2025 Motphys Technology Co., Ltd. All Rights Reserved.
#
# 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.
# ==============================================================================
"""PPO Agent Configuration for SKRL
This module provides configuration classes for PPO agents that match the
YAML configuration structure used in SKRL.
"""
from dataclasses import dataclass
from motrix_rl.base import BaseRLCfg
@dataclass
class PPOCfg(BaseRLCfg):
"""PPO configuration .
This class provides all the parameters needed to configure a PPO agent
in SKRL
"""
# Model architecture settings
policy_hidden_layer_sizes: tuple[int, ...] = (256, 128, 64)
value_hidden_layer_sizes: tuple[int, ...] = (256, 128, 64)
# Whether to share feature extraction layers between policy and value networks. only works if:
# 1. both networks have the same architecture
# 2. the backend is torch
share_policy_value_features: bool = True
# Agent settings
rollouts: int = 32
learning_epochs: int = 2
mini_batches: int = 32
discount_factor: float = 0.99
lambda_param: float = 0.95
# Learning rate settings
learning_rate: float = 1e-3
learning_rate_scheduler_kl_threshold: float = 0.008
# Training settings
random_timesteps: int = 0
learning_starts: int = 0
grad_norm_clip: float = 1.0
time_limit_bootstrap: bool = True
# PPO clipping settings
ratio_clip: float = 0.2
value_clip: float = 0.2
clip_predicted_values: bool = True
# Loss settings
entropy_loss_scale: float = 0.0
value_loss_scale: float = 2.0
kl_threshold: float = 0
# Reward shaping
rewards_shaper_scale: float = 1.0

View File

@@ -0,0 +1,29 @@
# Copyright (C) 2020-2025 Motphys Technology Co., Ltd. All Rights Reserved.
#
# 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.
# ==============================================================================
def _inherits_from(cls, base_class_name):
"""Check if cls inherits from a class with the given base_class_name."""
return any(base.__name__ == base_class_name for base in cls.__mro__)
def wrap_env(env, enable_render: bool = False):
"""Wrap the environment based on its type."""
if _inherits_from(env.__class__, "NpEnv"):
from motrix_rl.skrl.jax.wrap_np import SkrlNpWrapper
return SkrlNpWrapper(env, enable_render=enable_render)
else:
raise ValueError(f"Unsupported environment type: {env.__class__.__name__}")

View File

@@ -0,0 +1,300 @@
# Copyright (C) 2020-2025 Motphys Technology Co., Ltd. All Rights Reserved.
#
# 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.
# ==============================================================================
from typing import Any
import flax.linen as nn
import jax.numpy as jnp
import numpy as np
from skrl.agents.jax.ppo import PPO as BasePPO
from skrl.agents.jax.ppo import PPO_DEFAULT_CONFIG
from skrl.envs.jax import Wrapper
from skrl.memories.jax import RandomMemory
from skrl.models.jax import DeterministicMixin, GaussianMixin, Model
from skrl.resources.preprocessors.jax import RunningStandardScaler
from skrl.resources.schedulers.jax import KLAdaptiveRL
from skrl.trainers.jax import SequentialTrainer
from skrl.utils import set_seed
from motrix_envs import registry as env_registry
from motrix_rl import registry
from motrix_rl.skrl import get_log_dir
from motrix_rl.skrl.cfg import PPOCfg
from motrix_rl.skrl.jax import wrap_env
def _get_cfg(
rlcfg: PPOCfg,
env: Wrapper,
log_dir: str = None,
) -> dict:
# configure and instantiate the agent (visit its documentation to see all the options)
# https://skrl.readthedocs.io/en/latest/api/agents/ppo.html#configuration-and-hyperparameters
cfg = PPO_DEFAULT_CONFIG.copy()
cfg["rollouts"] = rlcfg.rollouts # memory_size
cfg["learning_epochs"] = rlcfg.learning_epochs
cfg["mini_batches"] = rlcfg.mini_batches # mini_batch_size = rollouts * num_envs / mini_batches
cfg["discount_factor"] = rlcfg.discount_factor
cfg["lambda"] = rlcfg.lambda_param
cfg["learning_rate"] = rlcfg.learning_rate
cfg["learning_rate_scheduler"] = KLAdaptiveRL
cfg["learning_rate_scheduler_kwargs"] = {"kl_threshold": rlcfg.learning_rate_scheduler_kl_threshold}
cfg["random_timesteps"] = rlcfg.random_timesteps
cfg["learning_starts"] = rlcfg.learning_starts
cfg["grad_norm_clip"] = rlcfg.grad_norm_clip
cfg["ratio_clip"] = rlcfg.ratio_clip
cfg["value_clip"] = rlcfg.value_clip
cfg["clip_predicted_values"] = rlcfg.clip_predicted_values
cfg["entropy_loss_scale"] = rlcfg.entropy_loss_scale
cfg["value_loss_scale"] = rlcfg.value_loss_scale
cfg["kl_threshold"] = rlcfg.kl_threshold
if rlcfg.rewards_shaper_scale != 1.0:
cfg["rewards_shaper"] = lambda reward, timestep, timesteps: reward * rlcfg.rewards_shaper_scale
else:
cfg["rewards_shaper"] = None
cfg["time_limit_bootstrap"] = rlcfg.time_limit_bootstrap
cfg["state_preprocessor"] = RunningStandardScaler
cfg["state_preprocessor_kwargs"] = {
"size": env.observation_space,
"device": env.device,
}
cfg["value_preprocessor"] = RunningStandardScaler
cfg["value_preprocessor_kwargs"] = {"size": 1, "device": env.device}
# logging to TensorBoard and write checkpoints (in timesteps)
if log_dir:
cfg["experiment"]["write_interval"] = rlcfg.check_point_interval
cfg["experiment"]["checkpoint_interval"] = rlcfg.check_point_interval
cfg["experiment"]["directory"] = log_dir
else:
cfg["experiment"]["write_interval"] = 0
cfg["experiment"]["checkpoint_interval"] = 0
return cfg
class PPO(BasePPO):
_total_custom_rewards: dict[str, np.ndarray] = {}
def record_transition(
self,
states,
actions,
rewards,
next_states,
terminated,
truncated,
infos,
timestep,
timesteps,
) -> None:
super().record_transition(
states,
actions,
rewards,
next_states,
terminated,
truncated,
infos,
timestep,
timesteps,
)
if "Reward" in infos:
for key, value in infos["Reward"].items():
self.tracking_data[f"Reward Instant / {key} (max)"].append(jnp.max(value))
self.tracking_data[f"Reward Instant / {key} (min)"].append(jnp.min(value))
self.tracking_data[f"Reward Instant / {key} (mean)"].append(jnp.mean(value))
if key not in self._total_custom_rewards:
self._total_custom_rewards[key] = jnp.zeros_like(value)
self._total_custom_rewards[key] += value
done = terminated | truncated
done = done.reshape(-1)
if done.any():
for key in self._total_custom_rewards:
self.tracking_data[f"Reward Total/ {key} (mean)"].append(
jnp.mean(self._total_custom_rewards[key][done])
)
self.tracking_data[f"Reward Total/ {key} (min)"].append(
jnp.min(self._total_custom_rewards[key][done])
)
self.tracking_data[f"Reward Total/ {key} (max)"].append(
jnp.max(self._total_custom_rewards[key][done])
)
self._total_custom_rewards[key] = self._total_custom_rewards[key] * (1 - done)
if "metrics" in infos:
for key, value in infos["metrics"].items():
self.tracking_data[f"metrics / {key} (max)"].append(jnp.max(value))
self.tracking_data[f"metrics / {key} (min)"].append(jnp.min(value))
self.tracking_data[f"metrics / {key} (mean)"].append(jnp.mean(value))
class Trainer:
_trainer: SequentialTrainer
_env_name: str
_sim_backend: str
_rlcfg: PPOCfg
_enable_render: bool
def __init__(
self,
env_name: str,
sim_backend: str = None,
enable_render: bool = False,
cfg_override: dict = None,
) -> None:
rlcfg = registry.default_rl_cfg(env_name, "skrl", backend="jax")
if cfg_override is not None:
rlcfg = rlcfg.replace(**cfg_override)
self._rlcfg = rlcfg
self._env_name = env_name
self._sim_backend = sim_backend
self._enable_render = enable_render
def train(self) -> None:
"""
Start training the agent.
"""
rlcfg = self._rlcfg
env = env_registry.make(self._env_name, sim_backend=self._sim_backend, num_envs=rlcfg.num_envs)
set_seed(rlcfg.seed)
skrl_env = wrap_env(env, self._enable_render)
models = self._make_model(skrl_env, rlcfg)
ppo_cfg = _get_cfg(rlcfg, skrl_env, log_dir=get_log_dir(self._env_name))
agent = self._make_agent(models, skrl_env, ppo_cfg)
cfg_trainer = {
"timesteps": rlcfg.max_batch_env_steps,
"headless": not self._enable_render,
}
trainer = SequentialTrainer(cfg=cfg_trainer, env=skrl_env, agents=agent)
trainer.train()
def play(self, policy: str) -> None:
import time
rlcfg = self._rlcfg
env = env_registry.make(self._env_name, sim_backend=self._sim_backend, num_envs=rlcfg.play_num_envs)
set_seed(rlcfg.seed)
env = wrap_env(env, self._enable_render)
models = self._make_model(env, rlcfg)
ppo_cfg = _get_cfg(rlcfg, env)
agent = self._make_agent(models, env, ppo_cfg)
agent.load(policy)
obs, _ = env.reset()
fps = 60
while True:
t = time.time()
outputs = agent.act(obs, timestep=0, timesteps=0)
actions = outputs[-1].get("mean_actions", outputs[0])
obs, _, _, _, _ = env.step(actions)
env.render()
delta_time = time.time() - t
if delta_time < 1.0 / fps:
time.sleep(1.0 / fps - delta_time)
def _make_model(self, env: Wrapper, rlcfg: PPOCfg) -> dict[str, Model]:
# define models (stochastic and deterministic models) using mixins
class Policy(GaussianMixin, Model):
def __init__(
self,
observation_space,
action_space,
device=None,
clip_actions=False,
clip_log_std=True,
min_log_std=-20,
max_log_std=2,
reduction="sum",
**kwargs,
):
Model.__init__(self, observation_space, action_space, device, **kwargs)
GaussianMixin.__init__(
self,
clip_actions,
clip_log_std,
min_log_std,
max_log_std,
reduction,
)
@nn.compact # marks the given module method allowing inlined submodules
def __call__(self, inputs, role):
x = inputs["states"]
for size in rlcfg.policy_hidden_layer_sizes:
x = nn.elu(nn.Dense(size)(x))
x = nn.Dense(self.num_actions)(x)
log_std = self.param("log_std", lambda _: jnp.ones(self.num_actions))
return x, log_std, {}
class Value(DeterministicMixin, Model):
def __init__(
self,
observation_space,
action_space,
device=None,
clip_actions=False,
**kwargs,
):
Model.__init__(self, observation_space, action_space, device, **kwargs)
DeterministicMixin.__init__(self, clip_actions)
@nn.compact # marks the given module method allowing inlined submodules
def __call__(self, inputs, role):
x = inputs["states"]
for size in rlcfg.value_hidden_layer_sizes:
x = nn.elu(nn.Dense(size)(x))
x = nn.Dense(1)(x)
return x, {}
models = {}
models["policy"] = Policy(
observation_space=env.observation_space,
action_space=env.action_space,
device=env.device,
)
models["value"] = Value(
observation_space=env.observation_space,
action_space=env.action_space,
device=env.device,
)
# instantiate models' state dict
for role, model in models.items():
model.init_state_dict(role)
return models
def _make_agent(self, models: dict[str, Model], env: Wrapper, ppo_cfg: dict[str, Any]) -> PPO:
memory = RandomMemory(memory_size=ppo_cfg["rollouts"], num_envs=env.num_envs, device=env.device)
agent = PPO(
models=models,
memory=memory,
cfg=ppo_cfg,
observation_space=env.observation_space,
action_space=env.action_space,
device=env.device,
)
return agent
class Player:
def __init__(self, env_name: str, sim_backend: str = None) -> None:
pass

View File

@@ -0,0 +1,80 @@
# Copyright (C) 2020-2025 Motphys Technology Co., Ltd. All Rights Reserved.
#
# 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.
# ==============================================================================
from typing import Any, Tuple
import gymnasium
import jax
import numpy as np
from skrl.envs.jax import Wrapper as SkrlWrapper
from motrix_envs.np.env import NpEnv
from motrix_envs.np.renderer import NpRenderer
class SkrlNpWrapper(SkrlWrapper):
"""
Wrap the numpy-based environment to be compatible with skrl
"""
_env: NpEnv
_renderer: NpRenderer = None
def __init__(self, env: NpEnv, enable_render: bool = False):
super().__init__(env)
if enable_render:
self._renderer = NpRenderer(env)
def reset(self) -> Tuple[jax.Array, Any]:
state = self._env.init_state()
return state.obs, state.info
def step(
self, actions: jax.Array
) -> Tuple[
jax.Array,
jax.Array,
jax.Array,
jax.Array,
Any,
]:
actions = np.array(actions)
state = self._env.step(actions)
return (
state.obs,
state.reward.reshape(-1, 1),
state.terminated.reshape(-1, 1),
state.truncated.reshape(-1, 1),
state.info,
)
def render(self, *args, **kwargs) -> Any:
if self._renderer:
self._renderer.render()
def close(self) -> None:
pass
@property
def num_envs(self) -> int:
return self._env.num_envs
@property
def observation_space(self) -> gymnasium.Space:
return self._env.observation_space
@property
def action_space(self) -> gymnasium.Space:
return self._env.action_space

View File

@@ -0,0 +1,29 @@
# Copyright (C) 2020-2025 Motphys Technology Co., Ltd. All Rights Reserved.
#
# 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.
# ==============================================================================
def _inherits_from(cls, base_class_name):
"""Check if cls inherits from a class with the given base_class_name."""
return any(base.__name__ == base_class_name for base in cls.__mro__)
def wrap_env(env, enable_render: bool = False):
"""Wrap the environment based on its type."""
if _inherits_from(env.__class__, "NpEnv"):
from motrix_rl.skrl.torch.wrap_np import SkrlNpWrapper
return SkrlNpWrapper(env, enable_render=enable_render)
else:
raise ValueError(f"Unsupported environment type: {env.__class__.__name__}")

View File

@@ -0,0 +1,355 @@
# Copyright (C) 2020-2025 Motphys Technology Co., Ltd. All Rights Reserved.
#
# 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.
# ==============================================================================
from typing import Any
import torch
import torch.nn as nn
from skrl.agents.torch.ppo import PPO as BasePPO
from skrl.agents.torch.ppo import PPO_DEFAULT_CONFIG
from skrl.envs.torch import Wrapper
from skrl.memories.torch import RandomMemory
from skrl.models.torch import DeterministicMixin, GaussianMixin, Model
from skrl.resources.preprocessors.torch import RunningStandardScaler
from skrl.resources.schedulers.torch import KLAdaptiveRL
from skrl.trainers.torch import SequentialTrainer
from skrl.utils import set_seed
from motrix_envs import registry as env_registry
from motrix_rl import registry
from motrix_rl.skrl import get_log_dir
from motrix_rl.skrl.cfg import PPOCfg
from motrix_rl.skrl.torch import wrap_env
def _get_cfg(
rlcfg: PPOCfg,
env: Wrapper,
log_dir: str = None,
) -> dict:
# configure and instantiate the agent (visit its documentation to see all the options)
# https://skrl.readthedocs.io/en/latest/api/agents/ppo.html#configuration-and-hyperparameters
cfg = PPO_DEFAULT_CONFIG.copy()
cfg["rollouts"] = rlcfg.rollouts # memory_size
cfg["learning_epochs"] = rlcfg.learning_epochs
cfg["mini_batches"] = rlcfg.mini_batches # mini_batch_size = rollouts * num_envs / mini_batches
cfg["discount_factor"] = rlcfg.discount_factor
cfg["lambda"] = rlcfg.lambda_param
cfg["learning_rate"] = rlcfg.learning_rate
cfg["learning_rate_scheduler"] = KLAdaptiveRL
cfg["learning_rate_scheduler_kwargs"] = {"kl_threshold": rlcfg.learning_rate_scheduler_kl_threshold}
cfg["random_timesteps"] = rlcfg.random_timesteps
cfg["learning_starts"] = rlcfg.learning_starts
cfg["grad_norm_clip"] = rlcfg.grad_norm_clip
cfg["ratio_clip"] = rlcfg.ratio_clip
cfg["value_clip"] = rlcfg.value_clip
cfg["clip_predicted_values"] = rlcfg.clip_predicted_values
cfg["entropy_loss_scale"] = rlcfg.entropy_loss_scale
cfg["value_loss_scale"] = rlcfg.value_loss_scale
cfg["kl_threshold"] = rlcfg.kl_threshold
if rlcfg.rewards_shaper_scale != 1.0:
cfg["rewards_shaper"] = lambda reward, timestep, timesteps: reward * rlcfg.rewards_shaper_scale
else:
cfg["rewards_shaper"] = None
cfg["time_limit_bootstrap"] = rlcfg.time_limit_bootstrap
cfg["state_preprocessor"] = RunningStandardScaler
cfg["state_preprocessor_kwargs"] = {
"size": env.observation_space,
"device": env.device,
}
cfg["value_preprocessor"] = RunningStandardScaler
cfg["value_preprocessor_kwargs"] = {"size": 1, "device": env.device}
# logging to TensorBoard and write checkpoints (in timesteps)
if log_dir:
cfg["experiment"]["write_interval"] = rlcfg.check_point_interval
cfg["experiment"]["checkpoint_interval"] = rlcfg.check_point_interval
cfg["experiment"]["directory"] = log_dir
else:
cfg["experiment"]["write_interval"] = 0
cfg["experiment"]["checkpoint_interval"] = 0
return cfg
class PPO(BasePPO):
_total_custom_rewards: dict[str, torch.Tensor] = {}
def record_transition(
self,
states,
actions,
rewards,
next_states,
terminated,
truncated,
infos,
timestep,
timesteps,
) -> None:
super().record_transition(
states,
actions,
rewards,
next_states,
terminated,
truncated,
infos,
timestep,
timesteps,
)
if "Reward" in infos:
for key, value in infos["Reward"].items():
value = torch.tensor(value, device=self.device)
self.tracking_data[f"Reward Instant / {key} (max)"].append(torch.max(value).item())
self.tracking_data[f"Reward Instant / {key} (min)"].append(torch.min(value).item())
self.tracking_data[f"Reward Instant / {key} (mean)"].append(torch.mean(value).item())
if key not in self._total_custom_rewards:
self._total_custom_rewards[key] = torch.zeros_like(value)
self._total_custom_rewards[key] += value
done = terminated | truncated
done = done.reshape(-1)
if done.any():
for key in self._total_custom_rewards:
self.tracking_data[f"Reward Total/ {key} (mean)"].append(
torch.mean(self._total_custom_rewards[key][done]).item()
)
self.tracking_data[f"Reward Total/ {key} (min)"].append(
torch.min(self._total_custom_rewards[key][done]).item()
)
self.tracking_data[f"Reward Total/ {key} (max)"].append(
torch.max(self._total_custom_rewards[key][done]).item()
)
self._total_custom_rewards[key] = self._total_custom_rewards[key] * (~done)
if "metrics" in infos:
for key, value in infos["metrics"].items():
tracked_value = torch.tensor(value, device=self.device)
self.tracking_data[f"metrics / {key} (max)"].append(torch.max(tracked_value).item())
self.tracking_data[f"metrics / {key} (min)"].append(torch.min(tracked_value).item())
self.tracking_data[f"metrics / {key} (mean)"].append(torch.mean(tracked_value).item())
class Trainer:
_trainer: SequentialTrainer
_env_name: str
_sim_backend: str
_rlcfg: PPOCfg
_enable_render: bool
def __init__(
self,
env_name: str,
sim_backend: str = None,
enable_render: bool = False,
cfg_override: dict = None,
) -> None:
rlcfg = registry.default_rl_cfg(env_name, "skrl", backend="torch")
if cfg_override is not None:
rlcfg = rlcfg.replace(**cfg_override)
self._rlcfg = rlcfg
self._env_name = env_name
self._sim_backend = sim_backend
self._enable_render = enable_render
def train(self) -> None:
"""
Start training the agent.
"""
rlcfg = self._rlcfg
env = env_registry.make(self._env_name, sim_backend=self._sim_backend, num_envs=rlcfg.num_envs)
set_seed(rlcfg.seed)
skrl_env = wrap_env(env, self._enable_render)
models = self._make_model(skrl_env, rlcfg)
ppo_cfg = _get_cfg(rlcfg, skrl_env, log_dir=get_log_dir(self._env_name))
agent = self._make_agent(models, skrl_env, ppo_cfg)
cfg_trainer = {
"timesteps": rlcfg.max_batch_env_steps,
"headless": not self._enable_render,
}
trainer = SequentialTrainer(cfg=cfg_trainer, env=skrl_env, agents=agent)
trainer.train()
def play(self, policy: str) -> None:
import time
rlcfg = self._rlcfg
env = env_registry.make(self._env_name, sim_backend=self._sim_backend, num_envs=rlcfg.play_num_envs)
set_seed(rlcfg.seed)
env = wrap_env(env, self._enable_render)
models = self._make_model(env, rlcfg)
ppo_cfg = _get_cfg(rlcfg, env)
agent = self._make_agent(models, env, ppo_cfg)
agent.load(policy)
with torch.no_grad():
obs, _ = env.reset()
fps = 60
while True:
t = time.time()
outputs = agent.act(obs, timestep=0, timesteps=0)
actions = outputs[-1].get("mean_actions", outputs[0])
obs, _, _, _, _ = env.step(actions)
env.render()
delta_time = time.time() - t
if delta_time < 1.0 / fps:
time.sleep(1.0 / fps - delta_time)
def _make_model(self, env: Wrapper, rlcfg: PPOCfg) -> dict[str, Model]:
def build_mlp(
input_size: int,
hidden_sizes: tuple[int, ...],
output_size: int,
activation=nn.ELU,
):
"""Helper function to build MLP layers."""
layers = []
current_size = input_size
for hidden_size in hidden_sizes:
layers.append(nn.Linear(current_size, hidden_size))
layers.append(activation())
current_size = hidden_size
layers.append(nn.Linear(current_size, output_size))
return nn.Sequential(*layers)
# define shared model (stochastic and deterministic models) using mixins
class Shared(GaussianMixin, DeterministicMixin, Model):
def __init__(
self,
observation_space,
action_space,
device,
policy_hidden_sizes,
value_hidden_sizes,
share_features=True,
clip_actions=False,
clip_log_std=True,
min_log_std=-20,
max_log_std=2,
reduction="sum",
):
Model.__init__(self, observation_space, action_space, device)
GaussianMixin.__init__(
self,
clip_actions,
clip_log_std,
min_log_std,
max_log_std,
reduction,
)
DeterministicMixin.__init__(self, clip_actions)
# Use configured share_features setting
self.share_features = share_features and policy_hidden_sizes == value_hidden_sizes
if self.share_features:
# Build shared feature extraction layers
shared_layers = []
current_size = self.num_observations
for hidden_size in policy_hidden_sizes:
shared_layers.append(nn.Linear(current_size, hidden_size))
shared_layers.append(nn.ELU())
current_size = hidden_size
self.net = nn.Sequential(*shared_layers)
self.mean_layer = nn.Linear(current_size, self.num_actions)
self.log_std_parameter = nn.Parameter(torch.ones(self.num_actions))
self.value_layer = nn.Linear(current_size, 1)
else:
# Build separate networks for policy and value
self.policy_net = build_mlp(
self.num_observations,
policy_hidden_sizes[:-1],
policy_hidden_sizes[-1] if len(policy_hidden_sizes) > 0 else self.num_actions,
)
self.value_net = build_mlp(
self.num_observations,
value_hidden_sizes[:-1],
value_hidden_sizes[-1] if len(value_hidden_sizes) > 0 else 1,
)
# Output layers
if len(policy_hidden_sizes) > 0:
self.mean_layer = nn.Linear(policy_hidden_sizes[-1], self.num_actions)
else:
self.mean_layer = nn.Linear(self.num_observations, self.num_actions)
self.log_std_parameter = nn.Parameter(torch.ones(self.num_actions))
if len(value_hidden_sizes) > 0:
self.value_layer = nn.Linear(value_hidden_sizes[-1], 1)
else:
self.value_layer = nn.Linear(self.num_observations, 1)
def act(self, inputs, role):
if role == "policy":
return GaussianMixin.act(self, inputs, role)
elif role == "value":
return DeterministicMixin.act(self, inputs, role)
def compute(self, inputs, role):
if role == "policy":
if self.share_features:
self._shared_output = self.net(inputs["states"])
return (
self.mean_layer(self._shared_output),
self.log_std_parameter,
{},
)
else:
policy_features = self.policy_net(inputs["states"])
return (
self.mean_layer(policy_features),
self.log_std_parameter,
{},
)
elif role == "value":
if self.share_features:
shared_output = (
self.net(inputs["states"]) if self._shared_output is None else self._shared_output
)
self._shared_output = None
return self.value_layer(shared_output), {}
else:
value_features = self.value_net(inputs["states"])
return self.value_layer(value_features), {}
models = {}
models["policy"] = Shared(
observation_space=env.observation_space,
action_space=env.action_space,
device=env.device,
policy_hidden_sizes=rlcfg.policy_hidden_layer_sizes,
value_hidden_sizes=rlcfg.value_hidden_layer_sizes,
share_features=rlcfg.share_policy_value_features,
)
models["value"] = models["policy"]
return models
def _make_agent(self, models: dict[str, Model], env: Wrapper, ppo_cfg: dict[str, Any]) -> PPO:
memory = RandomMemory(memory_size=ppo_cfg["rollouts"], num_envs=env.num_envs, device=env.device)
agent = PPO(
models=models,
memory=memory,
cfg=ppo_cfg,
observation_space=env.observation_space,
action_space=env.action_space,
device=env.device,
)
return agent

View File

@@ -0,0 +1,79 @@
# Copyright (C) 2020-2025 Motphys Technology Co., Ltd. All Rights Reserved.
#
# 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.
# ==============================================================================
from typing import Any, Tuple
import gymnasium
import torch
from skrl.envs.torch import Wrapper as SkrlWrapper
from motrix_envs.np.env import NpEnv
from motrix_envs.np.renderer import NpRenderer
class SkrlNpWrapper(SkrlWrapper):
"""
Wrap the numpy-based environment to be compatible with skrl (PyTorch)
"""
_env: NpEnv
_renderer: NpRenderer = None
def __init__(self, env: NpEnv, enable_render: bool = False):
super().__init__(env)
if enable_render:
self._renderer = NpRenderer(env)
def reset(self) -> Tuple[torch.Tensor, Any]:
state = self._env.init_state()
return torch.tensor(state.obs, dtype=torch.float32, device=self.device), state.info
def step(
self, actions: torch.Tensor
) -> Tuple[
torch.Tensor,
torch.Tensor,
torch.Tensor,
torch.Tensor,
Any,
]:
actions = actions.cpu().numpy()
state = self._env.step(actions)
return (
torch.tensor(state.obs, dtype=torch.float32, device=self.device),
torch.tensor(state.reward.reshape(-1, 1), dtype=torch.float32, device=self.device),
torch.tensor(state.terminated.reshape(-1, 1), dtype=torch.bool, device=self.device),
torch.tensor(state.truncated.reshape(-1, 1), dtype=torch.bool, device=self.device),
state.info,
)
def render(self, *args, **kwargs) -> Any:
if self._renderer:
self._renderer.render()
def close(self) -> None:
pass
@property
def num_envs(self) -> int:
return self._env.num_envs
@property
def observation_space(self) -> gymnasium.Space:
return self._env.observation_space
@property
def action_space(self) -> gymnasium.Space:
return self._env.action_space

View File

@@ -0,0 +1,61 @@
# Copyright (C) 2020-2025 Motphys Technology Co., Ltd. All Rights Reserved.
#
# 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.
# ==============================================================================
from dataclasses import dataclass
@dataclass
class DeviceSupports:
torch: bool = False
torch_gpu: bool = False
jax: bool = False
jax_gpu: bool = False
def _check_gpu_available_for_torch():
try:
import torch
if not torch.cuda.is_available():
return False
torch.zeros((1,)).cuda().numpy(force=True)
return True
except Exception:
return False
def get_device_supports() -> DeviceSupports:
supports = DeviceSupports()
try:
import torch # noqa: F401
supports.torch = True
supports.torch_gpu = _check_gpu_available_for_torch()
except ImportError:
pass
try:
import jax # noqa: F401
supports.jax = True
from jax.lib import xla_bridge
platform = xla_bridge.get_backend().platform
if platform == "gpu":
supports.jax_gpu = True
except ImportError:
pass
return supports

29
pyproject.toml Normal file
View File

@@ -0,0 +1,29 @@
[project]
name = "motrix-lab"
version = "0.0.1"
description = "A general-purpose machine learning architecture designed for robot training"
authors = [{ name = "Motphys", email = "developers@motphys.com" }]
requires-python = "==3.10.*"
[project.optional-dependencies]
docs = [
"sphinx",
"autodocsumm",
"pydata-sphinx-theme",
"myst-parser",
"sphinx-copybutton",
"sphinx-subfigure",
"sphinxcontrib-video",
"sphinx-togglebutton",
"sphinx-design",
]
[tool.uv.workspace]
members = ["motrix_envs", "motrix_rl"]
[tool.uv]
package = false
# for ReadtheDocs
[tool.setuptools]
packages = ["motrix_envs", "motrix_rl"]

22
ruff.toml Normal file
View File

@@ -0,0 +1,22 @@
# https://docs.astral.sh/ruff/settings/
line-length = 120
output-format = "concise"
exclude = ["*.pyi"]
[lint]
# Enable isort (I), Pyflakes (`F`) and pycodestyle (E, W)
select = ["I", "E", "F", "W"]
# E741: Ambiguous variable name
# https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules
ignore = ["E111", "E114", "E117", "E741"]
[lint.per-file-ignores]
# Ignore `F401` (imported but unused) in all `__init__.py` files`.
"__init__.py" = ["F401", "F403"]
[lint.isort]
known-first-party = ["motrix_envs", "motrix_rl"]
[format]
# Enable reformatting of code snippets in docstrings.
docstring-code-format = true

40
scripts/gpu_utils.py Normal file
View File

@@ -0,0 +1,40 @@
# Copyright (C) 2020-2025 Motphys Technology Co., Ltd. All Rights Reserved.
#
# 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.
# ==============================================================================
import jax.numpy as jnp
import pynvml
def monitor_gpu_utilization(stop_event, gpu_index=0, interval=1.0):
pynvml.nvmlInit()
handle = pynvml.nvmlDeviceGetHandleByIndex(gpu_index)
utilization_samples = []
while not stop_event.is_set():
util = pynvml.nvmlDeviceGetUtilizationRates(handle)
utilization_samples.append(util.gpu)
stop_event.wait(interval)
pynvml.nvmlShutdown()
if utilization_samples:
data = jnp.array(utilization_samples)
print(f"GPU utilization statistics over {len(data)} samples:")
print(f" Mean: {jnp.mean(data):.2f}%")
print(f" Max : {jnp.max(data):.2f}%")
print(f" Min : {jnp.min(data):.2f}%")
print(f" Median : {jnp.median(data):.2f}%")
else:
print("No GPU utilization samples recorded.")

Some files were not shown because too many files have changed in this diff Show More