chore: release v0.2.0

This commit is contained in:
motphys-developers
2026-02-10 08:08:11 +00:00
parent dbfa9e31fa
commit b568ac5600
123 changed files with 9732 additions and 497 deletions

10
.dockerignore Normal file
View File

@@ -0,0 +1,10 @@
# Exclude all files and directories.
*
# Unexclude specified files/directories.
!motrix_envs/**/*
!motrix_rl/**/*
!scripts/**/*
!pyproject.toml
!uv.lock
!uv.toml

124
.github/workflows/README.md vendored Normal file
View File

@@ -0,0 +1,124 @@
# GitHub Actions Workflows
This directory contains GitHub Actions workflows for automating CI/CD processes.
## Docker Image Build Workflow
### File: `docker-build.yml`
Automatically builds and pushes Docker images to Docker Hub when a new version tag is pushed.
#### Trigger Conditions
The workflow is triggered only when you push a git tag that matches the pattern `v*`:
```bash
git tag v0.1.0
git push origin v0.1.0
```
#### What It Does
1. **Extracts Version**: Reads the version from `pyproject.toml` (currently `0.1.0`)
2. **Builds Docker Image**: Uses the Dockerfile in `docker/Dockerfile`
3. **Pushes Multiple Tags**:
- `motphys/motrixlab:0.1.0` (version from pyproject.toml)
- `motphys/motrixlab:latest` (always points to the latest version)
- `motphys/motrixlab:0.1` (major.minor version)
#### Required Secrets
You need to configure the following secrets in your GitHub repository settings:
1. **`DOCKER_USERNAME`**: Your Docker Hub username
2. **`DOCKER_PASSWORD`**: Your Docker Hub password or access token
To add secrets:
1. Go to your repository on GitHub
2. Click **Settings****Secrets and variables****Actions**
3. Click **New repository secret**
4. Add the secrets listed above
#### Usage Example
```bash
# 1. Update version in pyproject.toml if needed
# 2. Commit your changes
git add .
git commit -m "Release v0.1.0"
# 3. Create and push a version tag
git tag v0.1.0
git push origin v0.1.0
# 4. The workflow will automatically build and push the Docker image
# 5. Monitor the build at: https://github.com/Motphys/MotrixLab/actions
```
#### Built Image Tags
After the workflow completes, the following Docker images will be available:
```bash
# Pull the latest version
docker pull motphys/motrixlab:latest
# Pull a specific version
docker pull motphys/motrixlab:0.1.0
# Pull major.minor version
docker pull motphys/motrixlab:0.1
```
#### Workflow Features
-**Optimized Caching**: Uses GitHub Actions cache to speed up builds
-**Multi-tag Support**: Automatically tags with version, major.minor, and latest
-**Version Extraction**: Automatically reads version from pyproject.toml
-**Docker Layer Caching**: Uses UV cache mounts for faster dependency installation
-**Tag-based Trigger**: Only builds on version tags, not on every commit
#### Docker Image Contents
The resulting Docker image includes:
- Base: NVIDIA CUDA 12.8.1 Runtime + Ubuntu 24.04
- UV package manager
- MotrixLab with all dependencies
- SKRL (both JAX and PyTorch backends)
- TensorBoard
- MotrixSim physics engine
#### Testing the Docker Image Locally
Before tagging a release, you can test the Docker build locally:
```bash
cd docker
docker build -t motphys/motrixlab:test .
docker run --gpus all motphys/motrixlab:test scripts/view.py --env cartpole
```
#### Troubleshooting
**Build fails with authentication error:**
- Verify Docker Hub credentials are correctly set in GitHub secrets
- Ensure your Docker Hub account has permission to push to the `motphys/motrixlab` repository
**Version extraction fails:**
- Ensure `pyproject.toml` has a valid `version = "x.y.z"` line
- Check the workflow logs for the exact extraction command output
**Tag not triggering the workflow:**
- Ensure the tag starts with `v` (e.g., `v0.1.0`, not `0.1.0`)
- Verify the tag was pushed to the correct branch: `git push origin v0.1.0`
#### See Also
- [Docker Hub Repository](https://hub.docker.com/r/motphys/motrixlab)
- [Container Deployment Documentation](../../docs/source/zh_CN/user_guide/getting_started/container_deployment.md)
- [Dockerfile](../../docker/Dockerfile)

60
.github/workflows/docker-build.yml vendored Normal file
View File

@@ -0,0 +1,60 @@
name: Build and Push Docker Image
on:
push:
tags:
- "v*"
env:
DOCKER_IMAGE: motphys/motrixlab
DOCKERFILE_PATH: docker
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Extract version from pyproject.toml
id: version
run: |
VERSION=$(grep '^version' pyproject.toml | head -1 | sed 's/version = "\(.*\)"/\1/' | tr -d '" ')
echo "version=${VERSION}" >> $GITHUB_OUTPUT
echo "Extracted version: ${VERSION}"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Extract metadata for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.DOCKER_IMAGE }}
tags: |
type=semver,pattern={{version}},prefix=v
type=semver,pattern={{major}}.{{minor}},prefix=v
type=raw,value=${{ steps.version.outputs.version }}
type=raw,value=latest
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: .
file: ${{ env.DOCKERFILE_PATH }}/Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
provenance: false

View File

@@ -1,7 +1,7 @@
version: 2
build:
os: ubuntu-22.04
os: ubuntu-24.04
tools:
python: "3.10"
jobs:
@@ -22,13 +22,14 @@ build:
# Make local files to have the real content on them
- ./tools/git-lfs checkout
- rm -rf ./tools
pre_create_environment:
- asdf plugin add uv
- asdf install uv latest
- asdf global uv latest
create_environment:
- uv venv "${READTHEDOCS_VIRTUALENV_PATH}"
install:
- UV_PROJECT_ENVIRONMENT="${READTHEDOCS_VIRTUALENV_PATH}" uv sync --frozen --extra docs
sphinx:
configuration: docs/source/conf.py
python:
install:
- method: pip
path: .
extra_requirements:
- docs

58
docker/Dockerfile Normal file
View File

@@ -0,0 +1,58 @@
FROM nvidia/cuda:12.8.1-runtime-ubuntu24.04
ENV DEBIAN_FRONTEND=noninteractive \
USERNAME=root \
HOMEDIR=/root \
UV_LINK_MODE=copy \
TZ=Asia/Shanghai
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
curl ca-certificates tzdata \
&& ln -fs /usr/share/zoneinfo/${TZ} /etc/localtime \
&& echo ${TZ} > /etc/timezone \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
COPY --from=ghcr.io/astral-sh/uv:0.9.14 /uv /uvx /bin/
WORKDIR ${HOMEDIR}/motrixlab
# =============================================================================
# Two-Stage Dependency Installation Strategy
# =============================================================================
#
# This Dockerfile uses a two-stage installation approach to optimize Docker layer
# caching and reduce rebuild times when source code changes.
#
# Stage 1 (below): Install heavy framework dependencies
# - Installs large ML frameworks (SKRL, JAX, PyTorch, TensorFlow) (~3GB)
# - Skips workspace members (motrix-envs, motrixsim) that depend on source code
# - These dependencies rarely change, so this layer is heavily cached
# - Mounts only configuration files (pyproject.toml, uv.lock), no source code
#
# Stage 2 (after source copy): Install workspace members and remaining dependencies
# - Installs all workspace members (motrix-envs, motrix-rl, motrixsim)
# - Source code changes only invalidate this stage, not Stage 1
# - Significantly faster rebuilds during development
#
# Benefits:
# - Source code changes don't require re-downloading 3GB+ of dependencies
# - Dependencies are cached across builds using UV cache
# =============================================================================
RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
--mount=type=bind,source=motrix_envs/pyproject.toml,target=motrix_envs/pyproject.toml \
--mount=type=bind,source=motrix_rl/pyproject.toml,target=motrix_rl/pyproject.toml \
--mount=type=bind,source=uv.toml,target=uv.toml \
--mount=type=bind,source=uv.lock,target=uv.lock \
uv sync --frozen --no-install-workspace --package motrix-rl --no-install-package motrix-envs --no-install-package motrixsim --extra skrl-jax --extra skrl-torch
COPY --chown=${USERNAME}:${USERNAME} . .
# Stage 2: Install workspace members with source code
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --all-packages --extra skrl-jax --extra skrl-torch
ENTRYPOINT [ "uv", "run" ]

41
docker/docker-compose.yml Normal file
View File

@@ -0,0 +1,41 @@
volumes:
motrixlab-data:
name: motrixlab-data
services:
motrixlab-training:
image: motphys/motrixlab:latest
container_name: motrixlab-training
volumes:
- motrixlab-data:/root/motrixlab/runs
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities: [gpu]
command:
- scripts/train.py
- --train-backend
- ${MOTRIX_TRAIN_BACKEND:-jax}
- --num-envs
- ${MOTRIX_NUM_ENVS:-2048}
- --env
- ${MOTRIX_ENV:-cartpole}
restart: no
motrixlab-tensorboard:
image: motphys/motrixlab:latest
container_name: motrixlab-tensorboard
ports:
- "6006:6006"
volumes:
- motrixlab-data:/root/motrixlab/runs:ro
command:
- tensorboard
- --host
- 0.0.0.0
- --logdir
- runs/${MOTRIX_ENV:-cartpole}
restart: unless-stopped

View File

@@ -78,22 +78,11 @@ for video_file in "$SOURCE_DIR"/*; do
echo "⏳ Processing '$filename'..."
# Extract first frame using ffmpeg with timeout
if [ -n "$FFMPEG_TIMEOUT" ]; then
# Use timeout if available
if timeout 30 ffmpeg -i "$video_file" -vframes 1 -q:v 2 "$output_file" -y -loglevel error 2>&1; then
ffmpeg_success=true
else
ffmpeg_success=false
fi
else
# No timeout available, run ffmpeg directly
if ffmpeg -i "$video_file" -vframes 1 -q:v 2 "$output_file" -y -loglevel error 2>&1; then
ffmpeg_success=true
else
ffmpeg_success=false
fi
fi
if $ffmpeg_success; then
# Verify the output file was created and is valid

View File

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

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4dd7992801e14afc2f4b71b29ce1618b04499c58702d2c151859b9005bd1b1e5
size 325888
oid sha256:8bde5a04286fbc690130f7051c623080ac4ee4dbe3891990444970a1e1449a14
size 293358

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f72f90d07336c646c8e32fb108b5e57afde29b69917e34892641853f3701075c
size 2784595
oid sha256:70b8d4d31a5694e2484904030c23d28c18ac90d85fcb151526214d14182f39d5
size 1519041

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -161,6 +161,20 @@ MotrixLab is a general-purpose machine learning architecture designed for robot
:::{grid-item-card}
```{video} _static/videos/dm_humanoid_run.mp4
:poster: _static/images/poster/dm_humanoid_run.jpg
:nocontrols:
:autoplay:
:playsinline:
:muted:
:loop:
:width: 100%
```
:::
:::{grid-item-card}
```{video} _static/videos/bounce_ball.mp4
:poster: _static/images/poster/bounce_ball.jpg
:nocontrols:

View File

@@ -0,0 +1,154 @@
# Acrobot
Acrobot is a two-link swing-up and balance task. The goal is to swing both arms up and reach a target position using one motor torque.
```{video} /_static/videos/acrobot.mp4
:poster: _static/images/poster/acrobot.jpg
:nocontrols:
:autoplay:
:playsinline:
:muted:
:loop:
:width: 100%
```
---
## Task Description
A two-link acrobot with one hinge joint is driven by a single motor. The motor is installed at the elbow joint, which is the only actuated joint in the system. The motor's torque rotates the rods in a plane, enabling swing-up from arbitrary initial angles and reaching a target position. Torque is limited by the actuator ctrlrange; by modulating its magnitude and direction, the policy must accumulate energy to swing up and reach the target while maintaining stability.
## Action Space
| Item | Details |
| ------------- | ------------------------------- |
| **Type** | `Box(-1.0, 1.0, (1,), float32)` |
| **Dimension** | 1 |
---
## Observation Space
| Item | Details |
| ------------- | ------------------------------- |
| **Type** | `Box(-inf, inf, (6,), float32)` |
| **Dimension** | 6 |
Order: `upper_arm_horizontal, lower_arm_horizontal, upper_arm_vertical, lower_arm_vertical, shoulder_velocity, elbow_velocity`.
---
## Reward Function Design
- Base sparse reward: encourages the tip to enter the target region (radius = 0.2)
- Continuous reward: provides 0.1 reward per step for staying in the target region
- Distance shaping: 0.3 \* (1.0 - clip(distance / 2.0, 0, 1.0)) to encourage movement towards target
- Velocity penalty: 0.01 \* max(0, velocity_magnitude - 2.0) to penalize excessive velocities
---
## Initial State
- Shoulder angle randomized in `[-pi, pi]`
- Elbow angle randomized in `[-pi, pi]`
- Angular velocities initialized to zero
## Episode Termination Conditions
- Episode length limited by `max_episode_seconds`
- NaN check for observation values
---
### 1. Environment Preview
```bash
uv run scripts/view.py --env acrobot
```
### 2. Start Training
```bash
# Train with default parameters
uv run scripts/train.py --env acrobot
# Customize parallel environments
uv run scripts/train.py --env acrobot --num-envs 1024
# Enable rendering during training
uv run scripts/train.py --env acrobot --render
```
### 3. View Training Progress
```bash
uv run tensorboard --logdir runs/acrobot
```
### 4. Test Training Results
```bash
# Auto-discover best policy (recommended)
uv run scripts/play.py --env acrobot
# Manually specify a policy file
uv run scripts/play.py --env acrobot --policy runs/acrobot/nn/best_policy.pickle
```
> **Tip**: Policies are auto-selected from `runs/acrobot/`. You can override with `--policy`.
---
## Configuration Parameters
### Environment Configuration
```python
@dataclass
class AcrobotEnvCfg(EnvCfg):
model_file: str = ".../acrobot.xml" # MJCF model
max_episode_seconds: float = 10.0
sim_dt: float = 0.01
ctrl_dt: float = 0.02
reset_noise_scale: float = 0.1
render_spacing: float = 2.0
```
### Training Configuration (PPO example)
```python
@rlcfg("acrobot", backend="jax")
@dataclass
class AcrobotPPO(PPOCfg):
max_env_steps: int = 60_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 = 64
learning_epochs: int = 5
mini_batches: int = 8
learning_rate: float = 3e-4
grad_norm_clip: float = 0.1
clip_predicted_values: bool = False
value_clip: float = 10.0
entropy_loss_scale: float = 0.1
learning_rate_scheduler_kl_threshold: float = 0.02
discount_factor: float = 0.995
lambda_param: float = 0.97
ratio_clip: float = 0.2
value_loss_scale: float = 0.5
random_timesteps: int = 0
learning_starts: int = 0
kl_threshold: float = 0.03
```
---
## Expected Training Results
1. Acrobot can swing up both arms to reach the target position
2. The tip can stay within the target region with stability
3. Excessive oscillations are reduced by velocity penalty
4. The policy efficiently approaches the target with smooth movements

View File

@@ -1,4 +1,4 @@
# ANYmal-C Locomotion
# ANYmal-C Navigation
## Overview

View File

@@ -42,46 +42,357 @@ The joints correspond as follows:
| Item | Details |
| ------------- | -------------------------------- |
| **Type** | `Box(-inf, inf, (25,), float32)` |
| **Dimension** | 25 |
| **Type** | `Box(-inf, inf, (29,), float32)` |
| **Dimension** | 29 |
The observation space consists of the following parts (in order):
| Part | Content Description | Dimension | Remarks |
| ----------- | ----------------------------------------------- | --------- | -------------------------------------------------------------------------------- |
| ----------------- | ----------------------------------------------- | --------- | -------------------------------------------------------------------------------- |
| **dof_pos** | Position information for each degree of freedom | 13 | First 6 are arm joints, last 7 are ball's free joint (3 position + 4 quaternion) |
| **dof_vel** | Velocity information for each degree of freedom | 12 | Velocity is derivative of position |
| **paddle_pos** | Paddle position information | 3 | x, y, z coordinates of paddle center |
| **target_height** | Target height | 1 | Target height for current environment |
| Index | Observation | Min Value | Max Value | XML Name | Type (Unit) |
| ----- | --------------------------- | --------- | --------- | ----------- | ------------------------- |
| ----- | ------------------------------------- | --------- | --------- | --------- | ------------------------ |
| 0-5 | Arm Joint Angles | -Inf | Inf | Joint1-6 | Angle (rad) |
| 6 | Ball x-coordinate | -Inf | Inf | ball_x | Position (m) |
| 7 | Ball y-coordinate | -Inf | Inf | ball_y | Position (m) |
| 8 | Ball z-coordinate | -Inf | Inf | ball_z | Position (m) |
| 9-12 | Ball Orientation Quaternion | -Inf | Inf | ball_qw/xyz | Quaternion (w,x,y,z) |
| 13-24 | Joint and Ball Velocities | -Inf | Inf | - | Velocity/Angular Velocity |
| 6-8 | Ball Position [x, y, z] | -Inf | Inf | ball_link | Position (m) |
| 9-12 | Ball Orientation Quaternion [w,x,y,z] | -Inf | Inf | ball_link | Quaternion |
| 13-18 | Arm Joint Angular Velocities | -Inf | Inf | Joint1-6 | Angular Velocity (rad/s) |
| 19-24 | Ball Velocity [vx,vy,vz,wx,wy,wz] | -Inf | Inf | ball_link | Velocity (m/s, rad/s) |
| 25-27 | Paddle Position [x, y, z] | -Inf | Inf | blocker | Position (m) |
| 28 | Target Height | -Inf | Inf | - | Position (m) |
---
## Reward Function Design
## Reward Function
The reward function consists of the following components:
The reward function uses a composite design with multiple reward and penalty terms to guide the robot to learn a stable ball bouncing strategy. All reward parameters can be adjusted through the configuration file.
```python
# Position Control Reward: Keep ball above paddle center
# Controlled Upward Velocity Reward: Reward moderate upward velocity when ball is well-positioned
# Height Accuracy Reward: Ball is close to target height
# Consecutive Bounces Reward: Reward consecutive successful bounces
# Total Reward = Weighted combination of all components
```
### Main Reward Terms
#### 1. Horizontal Position Reward
**Design Rationale**: This is the core reward term, ensuring the ball stays directly above the paddle. Through a vertical distance weighting mechanism, when the ball is close to the paddle (about to hit), the horizontal position requirement is stricter, guiding the strategy to align precisely at critical moments.
**Formula**:
$$
\begin{aligned}
\text{err}_{xy} &= \sqrt{(x_{ball} - x_{target})^2 + (y_{ball} - y_{target})^2} &&\text{(Horizontal position error)} \\
d_{vert} &= |z_{ball} - z_{paddle}| &&\text{(Vertical distance)} \\
w_{vert} &= e^{-d_{vert} / \sigma_{vert}} &&\text{(Vertical distance weight)} \\
\sigma_{pos} &= \sigma_{base} \times (1.0 + k_{weight} \times w_{vert}) &&\text{(Adaptive scale)} \\
r_{pos} &= e^{-\frac{\text{err}_{xy}^2}{2\sigma_{pos}^2}} &&\text{(Gaussian reward)} \\
\\
\text{Where:} \quad &\sigma_{vert} = 0.15 \text{ m} &&\text{(Vertical distance scale)} \\
&\sigma_{base} = 0.1 \text{ m} &&\text{(Base horizontal scale)} \\
&k_{weight} = 3.0 &&\text{(Weight factor)} \\
&x_{target} = 0.58856 \text{ m}, \, y_{target} = 0.0 \text{ m} &&\text{(Target position)}
\end{aligned}
$$
**Weight**: 2.0
#### 2. Out of Position Penalty
**Design Rationale**: Applies strong penalty for severe deviation from target position to prevent the ball from flying out of control range. Uses sigmoid function for smooth transition, avoiding discontinuous reward function.
**Formula**:
$$
r_{out} = -\frac{2.0}{1 + e^{-(\text{err}_{xy} - 0.05) / 0.03}} \qquad \text{(Sigmoid penalty)}
$$
**Weight**: 1.0
#### 3. Velocity Matching Reward
**Design Rationale**: Based on projectile motion physics, encourages the ball's trajectory to have the desired velocity (0.5 m/s) at target height. This ensures the ball doesn't pass through the target height too fast or too slow, facilitating stable control.
**Formula**:
$$
\begin{aligned}
\Delta h &= h_{target} - z_{ball} &&\text{(Height difference)} \\
v_{desired} &= 0.5 \text{ m/s} &&\text{(Desired velocity)} \\
\\
\text{Case 1:}&\text{ Ball moving upward and below target height} \\
v_{z,up}^2 &= v_z^2 - 2g\Delta h &&\text{(Energy conservation)} \\
v_{at\_target,up} &= \sqrt{\max(0, v_{z,up}^2)} &&\text{(Upward arrival velocity)} \\
\\
\text{Case 2:}&\text{ Ball moving downward and above target height} \\
v_{z,down}^2 &= v_z^2 + 2g|\Delta h| &&\text{(Energy conservation)} \\
v_{at\_target,down} &= -\sqrt{\max(0, v_{z,down}^2)} &&\text{(Downward arrival velocity)} \\
\\
\text{Case 3:}&\text{ Ball near target height (} |\Delta h| < 0.05 \text{ m)} \\
v_{at\_target,near} &= v_z &&\text{(Current velocity)} \\
\\
\text{Smooth combination:}& \\
\sigma_{up} &= \frac{1}{1 + e^{-v_z / 0.2}} &&\text{(Upward motion weight)} \\
\sigma_{below} &= \frac{1}{1 + e^{-\Delta h / 0.02}} &&\text{(Below target weight)} \\
\sigma_{down} &= 1 - \sigma_{up} &&\text{(Downward motion weight)} \\
\sigma_{above} &= 1 - \sigma_{below} &&\text{(Above target weight)} \\
w_{near} &= e^{-\frac{\Delta h^2}{2 \times 0.01^2}} &&\text{(Near target weight)} \\
\\
v_{at\_target} &= v_{at\_target,up} \cdot \sigma_{up} \cdot \sigma_{below} \\
&\quad + v_{at\_target,down} \cdot \sigma_{down} \cdot \sigma_{above} \\
&\quad + v_{at\_target,near} \cdot w_{near} &&\text{(Weighted combination)} \\
\\
\text{err}_{vel} &= |v_{at\_target} - v_{desired}| &&\text{(Velocity error)} \\
r_{vel} &= e^{-\frac{\text{err}_{vel}^2}{2 \times 0.8^2}} &&\text{(Gaussian reward)}
\end{aligned}
$$
**Weight**: 2.0
#### 4. Height Reward
**Design Rationale**: Directly encourages the ball to approach target height, one of the core task objectives. Higher weight (4.5) ensures the strategy prioritizes height control. Target height is randomly sampled (0.3-0.6 m) in each environment to improve policy generalization.
**Formula**:
$$
\begin{aligned}
\text{err}_h &= |z_{ball} - h_{target}| &&\text{(Height error)} \\
r_h &= e^{-\frac{\text{err}_h^2}{2 \times 0.15^2}} &&\text{(Gaussian reward)}
\end{aligned}
$$
**Weight**: 4.5
#### 5. Height Progress Reward
**Design Rationale**: Encourages the ball to reach higher positions, helping the strategy quickly learn to hit the ball upward in early training, avoiding the "no-hit" local optimum.
**Formula**:
$$
r_{progress} = \max(0, z_{ball} - 0.2) \times 2.0 \qquad \text{(Linear reward)}
$$
**Weight**: 1.0
#### 6. Controlled Upward Velocity Reward
**Design Rationale**: Only rewards upward velocity when the ball's horizontal position is good, avoiding "random hitting" behavior. Ideal velocity is calculated from physics formula, ensuring the ball can exactly reach target height. This reward guides the strategy to learn precise hitting force.
**Formula**:
$$
\begin{aligned}
q_{pos} &= e^{-\frac{\text{err}_{xy}^2}{2 \times 0.02^2}} &&\text{(Position quality)} \\
v_{ideal} &= \sqrt{2g \times \max(0, \Delta h)} &&\text{(Ideal launch velocity)} \\
v_{ideal} &\in [0.5, 3.0] \text{ m/s} &&\text{(Limit range)} \\
q_{vel} &= e^{-\frac{(v_z - v_{ideal})^2}{2 \times 0.5^2}} &&\text{(Velocity quality)} \\
\sigma_{up} &= \frac{1}{1 + e^{-v_z / 0.1}} &&\text{(Upward mask)} \\
r_{controlled} &= q_{pos} \times q_{vel} \times \sigma_{up} \times \text{clip}(v_z, 0, 1.5) &&\text{(Combined reward)}
\end{aligned}
$$
**Weight**: 1.5
#### 7. Consecutive Bounces Reward
**Design Rationale**: Encourages multiple consecutive successful bounces, guiding the strategy to learn stable long-term control. Uses logarithmic function to avoid infinite reward growth, while requiring good ball position to give reward.
**Formula**:
$$
\begin{aligned}
q_{bounce} &= e^{-\frac{\text{err}_{xy}^2}{2 \times 0.05^2}} &&\text{(Bounce position quality)} \\
r_{bounce\_log} &= 0.5 \times \log(n_{bounces} + 1) &&\text{(Logarithmic reward)} \\
r_{bounce} &= r_{bounce\_log} \times q_{bounce} \times \mathbb{1}_{n_{bounces} > 0} &&\text{(Conditional reward)}
\end{aligned}
$$
**Weight**: 0.8
#### 8. High Bounce Count Reward
**Design Rationale**: Gives extra reward for high bounce counts (≥3), further encouraging long-term stable control. Uses sigmoid activation function for smooth reward growth.
**Formula**:
$$
\begin{aligned}
\sigma_{high} &= \frac{1}{1 + e^{-(n_{bounces} - 2.0) / 0.5}} &&\text{(High bounce activation)} \\
r_{high} &= n_{bounces} \times 0.15 \times q_{bounce} \times \sigma_{high} &&\text{(Extra reward)}
\end{aligned}
$$
**Weight**: 0.3
#### 9. Paddle-Ball Horizontal Alignment Reward
**Design Rationale**: Encourages the paddle to actively move directly below the ball rather than waiting for the ball to fall. The closer the vertical distance, the greater the weight, requiring more precise alignment at the moment of hitting. Extra reward (boost) is given at bounce moment to reinforce correct hitting behavior.
**Formula**:
$$
\begin{aligned}
\text{err}_{align} &= \|(\mathbf{x}_{ball}, \mathbf{y}_{ball}) - (\mathbf{x}_{paddle}, \mathbf{y}_{paddle})\|_2 &&\text{(Alignment error)} \\
w_{prox} &= e^{-d_{vert} / 0.1} &&\text{(Vertical proximity weight)} \\
q_{align} &= e^{-\frac{\text{err}_{align}^2}{2 \times 0.03^2}} \times (1.0 + 2.0 \times w_{prox}) &&\text{(Alignment quality)} \\
k_{boost} &= \begin{cases} 3.0 & \text{if bounce detected} \\ 1.0 & \text{otherwise} \end{cases} &&\text{(Bounce boost)} \\
r_{align} &= q_{align} \times k_{boost} \times 0.3 &&\text{(Alignment reward)}
\end{aligned}
$$
**Weight**: 0.6
#### 10. Paddle Home Position Reward
**Design Rationale**: Encourages the paddle to return to home position when the ball is far away, avoiding the paddle staying at high position for long time. Uses distance dynamic factor: when ball is far from paddle ($d_{vert} > 0.15$ m), increase reward to encourage quick return; when ball is close, decrease reward to allow paddle to move up for hitting. This design makes paddle motion more energy-efficient and natural.
**Formula**:
$$
\begin{aligned}
\text{err}_{home} &= |z_{paddle} - z_{home}| &&\text{(Home deviation)} \\
k_{dist} &= 1.0 + \frac{0.5}{1 + e^{-(d_{vert} - 0.15) / 0.03}} &&\text{(Distance factor)} \\
q_{home} &= e^{-\frac{\text{err}_{home}^2}{2 \times 0.05^2}} &&\text{(Home quality)} \\
r_{home} &= q_{home} \times k_{dist} &&\text{(Home reward)} \\
\\
\text{Where:} \quad &z_{home} = 0.05 \text{ m} &&\text{(Paddle home height)}
\end{aligned}
$$
**Weight**: 1.5
### Penalty Terms
#### 1. Excessive Upward Velocity Penalty
**Design Rationale**: Prevents ball velocity from being too fast (>3.5 m/s) and losing control, ensuring ball motion stays within controllable range.
**Formula**:
$$
r_{excess} = -\frac{1.0}{1 + e^{-(v_z - 3.5) / 0.3}} \qquad \text{(Sigmoid penalty)}
$$
**Weight**: 1.0
#### 2. Downward Velocity Penalty
**Design Rationale**: Penalizes ball moving downward ($v_z < -0.2$ m/s), encouraging the strategy to hit the ball in time, avoiding free fall.
**Formula**:
$$
\begin{aligned}
\text{mag}_{down} &= -v_z \times \text{clip}(-v_z \times 0.3, 0, 0.5) &&\text{(Downward magnitude)} \\
\sigma_{down} &= \frac{1}{1 + e^{(v_z + 0.2) / 0.2}} &&\text{(Downward activation)} \\
r_{down} &= \text{mag}_{down} \times \sigma_{down} &&\text{(Downward penalty)}
\end{aligned}
$$
**Weight**: 1.0
#### 3. Paddle Height Violation Penalty
**Design Rationale**: Applies strong penalty when paddle deviates too far from home position (>0.1 m), ensuring paddle doesn't stay at high position for long time.
**Formula**:
$$
\begin{aligned}
\text{viol}_{height} &= \max(0, \text{err}_{home} - 0.1) &&\text{(Violation amount)} \\
r_{violation} &= -20.0 \times \text{viol}_{height} &&\text{(Strong penalty)}
\end{aligned}
$$
**Weight**: 1.0
#### 4. Action Change Rate Penalty
**Design Rationale**: Penalizes drastic action changes, encouraging smooth control strategy.
**Formula**:
$$
\begin{aligned}
r_{action} &= -\|\mathbf{a}_t - \mathbf{a}_{t-1}\|^2 &&\text{(L2 penalty)} \\
\\
\text{Where:} \quad &\mathbf{a}_t \in \mathbb{R}^6 &&\text{(Current action vector)}
\end{aligned}
$$
**Weight**: $10^{-4}$
#### 5. Joint Velocity Penalty
**Design Rationale**: Penalizes excessive joint velocities, encouraging energy-efficient and smooth motion.
**Formula**:
$$
\begin{aligned}
r_{joint\_vel} &= -\sum_{i=1}^{6} \dot{q}_i^2 &&\text{(Velocity squared sum penalty)} \\
\\
\text{Where:} \quad &\dot{\mathbf{q}} = [\dot{q}_1, \dot{q}_2, \dot{q}_3, \dot{q}_4, \dot{q}_5, \dot{q}_6]^T &&\text{(Joint angular velocity vector)}
\end{aligned}
$$
**Weight**: $10^{-4}$
### Total Reward Calculation
$$
\begin{aligned}
R_{total} = &\ 2.0 \cdot r_{pos} \\
&+ 1.0 \cdot r_{out} \\
&+ 2.0 \cdot r_{vel} \\
&+ 4.5 \cdot r_h \\
&+ 1.0 \cdot r_{progress} \\
&+ 1.5 \cdot r_{controlled} \\
&+ 0.8 \cdot r_{bounce} \\
&+ 0.3 \cdot r_{high} \\
&+ 0.6 \cdot r_{align} \\
&+ 1.5 \cdot r_{home} \\
&+ 1.0 \cdot r_{excess} \\
&+ 1.0 \cdot r_{down} \\
&+ 1.0 \cdot r_{violation} \\
&+ 10^{-4} \cdot r_{action} \\
&+ 10^{-4} \cdot r_{joint\_vel}
\end{aligned}
$$
---
## Initial State
- **Arm Initial Position**: [0, 40, 110, 0, -60, 0] degrees, with random noise
- **Ball Initial Position**: Above paddle center, with random noise
- **Ball Initial Velocity**: [0.0, 0.0, 0.0] m/s
### Robot Initialization
**Joint Angles**:
- Default angles: [0°, 40°, 110°, 0°, -60°, 0°]
- Random noise: Uniform random noise in [-0.1, 0.1] radians added to each joint
**Joint Velocities**:
- Initialized to zero with small random noise
### Ball Initialization
**Position**:
- Base position: `ball_init_pos` from config file (default [0.58856, 0, 0.45] m)
- Random noise: Uniform random noise in [-0.01, 0.01] m
**Velocity**:
- Initialized to zero
**Orientation**:
- Quaternion: [0, 0, 0, 1] (identity quaternion, no rotation)
### Target Height
Target height for each environment is randomly sampled in [0.4, 0.6] m range to improve policy generalization.
---
@@ -89,7 +400,9 @@ The reward function consists of the following components:
- **Ball Falls**: Ball z-coordinate < 0.05m (near ground)
- **Ball Too High**: Ball z-coordinate > target height + 1.0m (lost control)
- **Horizontal Deviation Too Far**: Absolute value of ball x-coordinate > 1.5m
- **Horizontal Deviation Too Far**: Ball x or y coordinate absolute value > 1.5m
- **Joint Velocity Too High**: Any joint angular velocity > 2π rad/s (360°/s)
- **Timeout**: Episode duration exceeds maximum allowed time
---
@@ -128,3 +441,9 @@ uv run scripts/play.py --env bounce_ball
3. Height Control: Ball's height stable within target height 0.8 ± 0.1m range
4. Velocity Control: Ball's upward velocity maintained within reasonable range (0.1-1.5 m/s)
5. Stable Control: Capable of maintaining stable bouncing for 20 seconds without dropping
---
## Known Issues
- **JAX Backend Training Performance**: The JAX version currently shows suboptimal training performance. For better results, it is recommended to use the PyTorch backend for this environment.

View File

@@ -0,0 +1,167 @@
# Manipulator Bring Ball
Bring Ball is a classic Manipulator task from the DeepMind Control Suite. A planar hand with a thumb and finger must grasp a ball and move it to a target ball position. MotrixLab currently provides one Bring Ball environment:
- `dm-manipulator-bring-ball`: grasp the ball and move it to the target position
```{video} /_static/videos/bring-ball.mp4
:poster: _static/images/poster/bring-ball.jpg
:nocontrols:
:autoplay:
:playsinline:
:muted:
:loop:
:width: 100%
```
---
## Task Description
Bring Ball is a planar (x-z) grasp-and-transport task:
- The hand has 4 arm joints (`arm_root`, `arm_shoulder`, `arm_elbow`, `arm_wrist`) plus thumb/finger joints
- Grasping is driven by the `grasp` tendon, coupling the `thumb` and `finger` joints
- The ball can slide in the plane (`ball_x`, `ball_z`) and rotate about the y-axis (`ball_y`)
- The target ball is a mocap body, sampled at reset
---
## Action Space
| Item | Details |
| ------------- | ------------------------------- |
| **Type** | `Box(-1.0, 1.0, (5,), float32)` |
| **Dimension** | 5 |
Actions correspond to the following actuators:
| Index | Action Meaning | Min Control | Max Control | XML Name |
| ----: | ---------------------------------- | :---------: | :---------: | :--------: |
| 0 | Root joint drive | -1 | 1 | `root` |
| 1 | Shoulder joint drive | -1 | 1 | `shoulder` |
| 2 | Elbow joint drive | -1 | 1 | `elbow` |
| 3 | Wrist joint drive | -1 | 1 | `wrist` |
| 4 | Grasp drive (thumb/finger coupled) | -1 | 1 | `grasp` |
---
## Observation Space
| Item | Details |
| ------------- | -------------------------------- |
| **Type** | `Box(-inf, inf, (41,), float32)` |
| **Dimension** | 41 |
The observation vector is composed of the following parts (in order):
| Part | Content Description | Dim | Notes |
| -------------- | ----------------------------- | --- | ------------------------------------ |
| **arm_pos** | `sin`/`cos` of 8 joint angles | 16 | Joint order listed below |
| **arm_vel** | 8 joint velocities | 8 | Same order as arm_pos |
| **touch** | `log(1 + touch)` sensors | 5 | palm/finger/thumb/fingertip/thumbtip |
| **hand_pos** | Grasp site world position | 3 | x, y, z |
| **object_pos** | Ball position | 3 | x, y, z |
| **target_pos** | Target ball position | 3 | x, y, z |
| **rel** | `object_pos - target_pos` | 3 | Relative position |
| Index | Observation Range | Dim | Notes |
| ----- | ----------------------------------------------- | --- | --------------------------------- |
| 0-15 | `sin`/`cos` of 8 joint angles | 16 | Joint order: arm_root -> thumbtip |
| 16-23 | 8 joint velocities | 8 | Same order as above |
| 24-28 | Touch: palm, finger, thumb, fingertip, thumbtip | 5 | `log(1 + touch)` |
| 29-31 | Grasp position (x, y, z) | 3 | hand_pos |
| 32-34 | Ball position (x, y, z) | 3 | object_pos |
| 35-37 | Target ball position (x, y, z) | 3 | target_pos |
| 38-40 | Ball relative position (x, y, z) | 3 | rel |
Joint order: `arm_root`, `arm_shoulder`, `arm_elbow`, `arm_wrist`, `finger`, `fingertip`, `thumb`, `thumbtip`.
---
## Reward Function Design
Bring Ball uses shaped rewards with multiple components and penalties:
```python
# R1: Reach - fingertips approach the ball
r_reach = tolerance(avg_tip_dist)
# R2: Orient - palm points toward the ball
r_orient = clip(1 - orient_bound + dot(hand_dir, unit_vec_to_ball), 0..1)
# R3: Pause - reduce arm jitter when close to the ball
r_pause = tolerance(arm_speed_step) * is_close_to_ball
# R4: Close - grasp intent and contact together
r_close = r_close_intent * (approach_or_grasp)
# R5: Lift & Transport - height and target distance
r_lift_height = tolerance(ball_z)
r_transport = tolerance(move_dist_to_target)
r_lift = mix(r_lift_height, r_transport)
# Precision/Progress
r_precision = tolerance(move_dist_to_target, gaussian)
r_progress = (prev_dist - curr_dist) * scale
# Penalties
penalty_side + penalty_hover
```
Default weights (from `BringBallCfg`):
- reach 1.0, orient 1.5, pause 0.5, close 2.0, lift 6.0, precision 1.0
- lift mixes `lift_height_weight` and `transport_weight`
- progress reward is controlled by `transport_progress_scale`
---
## Initial State
- **Arm initialization**: uses the default model pose (`randomize_arm=False`), thumb/finger are symmetric
- **Target position**: `x in [-0.4, 0.4]`, `z in [0.1, 0.4]`, `y = 0.001`
- **Ball position**: `x in [-0.4, 0.4]`, `z in [0.2, 0.7]`, with a minimum hand distance
- **Physics settling**: performs settle steps after reset (`settle_steps=300`)
---
## Episode Termination Conditions
- Terminate if any `NaN` appears in observations
---
## Usage Guide
### 1. Environment Preview (random actions)
```bash
uv run scripts/view.py --env dm-manipulator-bring-ball
```
### 2. Start Training
```bash
uv run scripts/train.py --env dm-manipulator-bring-ball --train-backend torch
```
### 3. View Training Progress
```bash
uv run tensorboard --logdir runs/dm-manipulator-bring-ball
```
### 4. Test Training Results
```bash
uv run scripts/play.py --env dm-manipulator-bring-ball
```
---
## Expected Training Results
1. The hand consistently reaches and grasps the ball
2. The ball is lifted off the ground and held at a stable height
3. The ball is reliably moved close to the target ball position

View File

@@ -127,5 +127,6 @@ uv run scripts/play.py --env dm-cheetah
## Expected Training Results
1. Stable horizontal speed approaching or exceeding 30.0 m/s
2. Maintaining an upright torso and coordinated gait
1. Run at a stable horizontal speed close to or exceeding 10.0 m/s
2. Maintain torso stability and coordinated gait, running long distances without falling
3. Running posture close to that of a real cheetah, with a sense of extension during the run

View File

@@ -0,0 +1,179 @@
# Finger Manipulation
Finger is a classic manipulation task from the DeepMind Control Suite. A two-link “finger” applies torques to interact with a rotating spinner. MotrixLab currently provides three Finger environments:
```{video} /_static/videos/dm_finger_spin.mp4
:poster: _static/images/poster/dm_finger_spin.jpg
:nocontrols:
:autoplay:
:playsinline:
:muted:
:loop:
:width: 100%
```
```{video} /_static/videos/dm_finger_turn.mp4
:poster: _static/images/poster/dm_finger_turn.jpg
:nocontrols:
:autoplay:
:playsinline:
:muted:
:loop:
:width: 100%
```
- `dm-finger-spin`: make the spinner rotate continuously in the target direction
- `dm-finger-turn-easy`: align the spinner tip (`tip`) with a target point (larger target radius)
- `dm-finger-turn-hard`: same as Turn, but with a smaller target radius
---
## Task Description
Finger is a planar (x-z) interaction task:
- The finger has 2 actuated hinge joints: `proximal` and `distal`
- The spinner rotates around joint `hinge`, and `tip` denotes the spinner tip position
- For Turn tasks, a target point is sampled around the spinner at the beginning of each episode
---
## Action Space
| Item | Details |
| ------------- | ------------------------------- |
| **Type** | `Box(-1.0, 1.0, (2,), float32)` |
| **Dimension** | 2 |
The actions correspond to:
| Index | Action Description | Min Control | Max Control | XML Name | Joint Type |
| ----- | ---------------------------------- | ----------- | ----------- | -------- | ---------- |
| 0 | Torque applied to `proximal` joint | -1 | 1 | proximal | hinge |
| 1 | Torque applied to `distal` joint | -1 | 1 | distal | hinge |
---
## Observation Space
MotrixLab follows dm_control-style observations, but flattens them into a single vector.
### Spin Observation Space
| Item | Details |
| ------------- | ------------------------------- |
| **Type** | `Box(-inf, inf, (9,), float32)` |
| **Dimension** | 9 |
The observation vector contains (in order):
- **position (4)**: `qpos(proximal, distal)` + `tip_xz` (tip position relative to the spinner in x-z)
- **velocity (3)**: `qvel(proximal, distal, hinge)` (hinge velocity is used by Spin reward)
- **touch (2)**: `log(1 + touchtop)`, `log(1 + touchbottom)`
### Turn Observation Space
| Item | Details |
| ------------- | -------------------------------- |
| **Type** | `Box(-inf, inf, (12,), float32)` |
| **Dimension** | 12 |
Compared to Spin, Turn adds:
- **target_position (2)**: target position relative to the spinner in x-z
- **dist_to_target (1)**: signed distance from tip to the target sphere surface (negative means “inside”)
---
## Reward Function Design
### Spin
In dm_control, Spin is typically defined with a sparse threshold on spinner angular velocity. MotrixLab defaults to a dense/shaped reward for easier training, while also logging the sparse version:
```text
spin_sparse = 1 if hinge_velocity <= -15 else 0
spin = clip(-hinge_velocity / 15, 0, 1)
```
### Turn (Easy / Hard)
Turn aims to bring the spinner tip into a target sphere around the spinner:
- `turn_sparse = 1` when `dist_to_target <= 0`
- MotrixLab defaults to a shaped reward based on distance-to-target (exponential decay), and adds auxiliary terms to reduce “no-contact” failure modes and action jitter:
- approach-to-spinner shaping
- touch bonus
- action magnitude / action change penalties
The final shaped reward is clipped to `[0, 1]`.
---
## Initial State
- `proximal`, `distal` joint angles are sampled uniformly within joint limits
- spinner `hinge` angle is sampled uniformly in `[-pi, pi]`
- for Turn tasks, the target is sampled around the spinner on the x-z plane at reset
---
## Episode Termination Conditions
### Termination
If `NaN` appears in the observations
---
## Usage Guide
### 1. Environment Preview (random actions)
```bash
uv run scripts/view.py --env dm-finger-spin
```
```bash
uv run scripts/view.py --env dm-finger-turn-easy
```
```bash
uv run scripts/view.py --env dm-finger-turn-hard
```
### 2. Start Training
```bash
uv run scripts/train.py --env dm-finger-spin --train-backend torch
```
```bash
uv run scripts/train.py --env dm-finger-turn-easy --train-backend torch
```
```bash
uv run scripts/train.py --env dm-finger-turn-hard --train-backend torch
```
### 3. View Training Progress
```bash
uv run tensorboard --logdir runs/dm-finger-spin
```
### 4. Test Training Results
`scripts/play.py` will auto-discover the latest `best_agent.*` under `runs/{env-name}/` (or you can pass `--policy` explicitly):
```bash
uv run scripts/play.py --env dm-finger-turn-hard
```
---
## Expected Training Results
1. `dm-finger-spin`: stable continuous rotation in the target direction
2. `dm-finger-turn-easy`: consistent contact and alignment with the (larger) target region, with reduced jitter
3. `dm-finger-turn-hard`: successful alignment with a smaller target region, typically requiring more training and better contact behavior

View File

@@ -0,0 +1,236 @@
# 3D Humanoid Robot
The 3D Humanoid Robot (Humanoid) is a classic bipedal locomotion task from DeepMind Control Suite. The goal is to train a simulated 3D humanoid robot to achieve standing, walking, and running by controlling joint torques.
```{video} /_static/videos/dm_humanoid_run.mp4
:poster: _static/images/poster/dm_humanoid_run.jpg
:nocontrols:
:autoplay:
:playsinline:
:muted:
:loop:
:width: 100%
```
## Task Description
Humanoid is a 3D bipedal humanoid robot task. The robot consists of a head, torso, two arms, and two legs, with 21 controlled joints (actuators). The agent controls the robot by applying torques to these joints to achieve standing balance, forward walking, or fast running. This task requires coordinated bipedal gait, balance control, and 3D spatial posture stability.
---
## Action Space
| Item | Details |
| -------- | -------------------------------- |
| **Type** | `Box(-1.0, 1.0, (21,), float32)` |
| **Dim** | 21 |
Actions correspond to the following:
| Index | Action (Joint Torque) | Min | Max | XML Name |
| ----: | ------------------------------ | :--: | :-: | :---------------- |
| 0 | Abdomen Y-axis rotation torque | -1.0 | 1.0 | `abdomen_y` |
| 1 | Abdomen Z-axis rotation torque | -1.0 | 1.0 | `abdomen_z` |
| 2 | Abdomen X-axis rotation torque | -1.0 | 1.0 | `abdomen_x` |
| 3 | Right hip X-axis torque | -1.0 | 1.0 | `right_hip_x` |
| 4 | Right hip Z-axis torque | -1.0 | 1.0 | `right_hip_z` |
| 5 | Right hip Y-axis torque | -1.0 | 1.0 | `right_hip_y` |
| 6 | Right knee torque | -1.0 | 1.0 | `right_knee` |
| 7 | Right ankle X-axis torque | -1.0 | 1.0 | `right_ankle_x` |
| 8 | Right ankle Y-axis torque | -1.0 | 1.0 | `right_ankle_y` |
| 9 | Left hip X-axis torque | -1.0 | 1.0 | `left_hip_x` |
| 10 | Left hip Z-axis torque | -1.0 | 1.0 | `left_hip_z` |
| 11 | Left hip Y-axis torque | -1.0 | 1.0 | `left_hip_y` |
| 12 | Left knee torque | -1.0 | 1.0 | `left_knee` |
| 13 | Left ankle X-axis torque | -1.0 | 1.0 | `left_ankle_x` |
| 14 | Left ankle Y-axis torque | -1.0 | 1.0 | `left_ankle_y` |
| 15 | Right shoulder 1 torque | -1.0 | 1.0 | `right_shoulder1` |
| 16 | Right shoulder 2 torque | -1.0 | 1.0 | `right_shoulder2` |
| 17 | Right elbow torque | -1.0 | 1.0 | `right_elbow` |
| 18 | Left shoulder 1 torque | -1.0 | 1.0 | `left_shoulder1` |
| 19 | Left shoulder 2 torque | -1.0 | 1.0 | `left_shoulder2` |
| 20 | Left elbow torque | -1.0 | 1.0 | `left_elbow` |
---
## Observation Space
| Item | Details |
| -------- | -------------------------------- |
| **Type** | `Box(-inf, inf, (73,), float32)` |
| **Dim** | 73 |
The observation space of the Humanoid environment consists of the following components (in order):
| Component | Description | Dim | Notes |
| ------------------ | --------------------------------------- | --- | --------------------------------------------------------------------- |
| **joint_angles** | Joint angles (excluding root's 7 DOF) | 22 | Angles of 22 joints |
| **head_height** | Head height | 1 | Height of head relative to ground |
| **extremities** | Extremity positions (relative to torso) | 12 | Left hand, left foot, right hand, right foot (3D each, in that order) |
| **torso_vertical** | Torso vertical direction vector | 3 | Vertical direction in local coordinates |
| **com_vel** | Center of mass linear velocity | 3 | Linear velocity of torso subtree |
| **qvel** | Velocities of all joints and root | 29 | Including root's 6 DOF |
| **target_local** | Target direction (local coordinates) | 3 | Target direction in torso local frame |
---
## Reward Function Design
The Humanoid reward function varies according to task type (standing, walking, running), but all include the following core components:
### Posture Reward
```python
# Head height reward: keep head above target height (95% of stand_height, ~1.33m)
stand_reward = tolerance(head_height, bounds=(stand_height * 0.95, inf), margin=0.5)
# Torso upright reward: keep torso upright
upright_reward = tolerance(torso_upright, bounds=(0.9, inf), sigmoid="linear", margin=0.9)
# Pelvis height reward: keep pelvis at reasonable height (60% of stand_height, ~0.84m)
pelvis_height_reward = tolerance(pelvis_height, bounds=(stand_height * 0.6, inf), sigmoid="linear", margin=stand_height * 0.6)
# Posture reward = head height reward × torso upright reward × pelvis height reward
posture_reward = stand_reward * upright_reward * pelvis_height_reward
```
### Speed Reward
The speed reward calculation differs by task type:
**Standing Task (move_speed <= 0)**:
```python
# Speed reward: maintain near-zero speed
speed_reward = tolerance(actual_speed, bounds=(0, 0), margin=1.0, value_at_margin=0.01)
```
**Walking Task (0 < move_speed <= 3.0)**:
```python
# Speed reward: achieve target speed (default 1.0 m/s) in target direction (positive X-axis)
actual_speed = dot(com_vel[:2], target_direction[:2]) # Projection of velocity onto target direction
speed_reward = tolerance(actual_speed, bounds=(move_speed, move_speed), margin=move_speed, sigmoid="linear")
```
**Running Task (move_speed > 3.0)**:
```python
# Speed reward: achieve target speed (default 10.0 m/s) or above in target direction
actual_speed = dot(com_vel[:2], target_direction[:2])
speed_reward = tolerance(actual_speed, bounds=(move_speed, inf), margin=move_speed, sigmoid="linear")
```
### Energy Reward
```python
energy_reward = exp(-energy_coef * mean(ctrls ^ 2))
```
### Gait Reward
```python
# Torso heading reward: torso faces target direction
torso_heading_reward = tolerance(dot(torso_forward, target_dir), bounds=(0.9, 1.0), margin=0.3, sigmoid="linear")
# Head heading reward: head faces target direction
head_heading_reward = tolerance(dot(head_forward, target_dir), bounds=(0.9, 1.0), margin=0.3, sigmoid="linear")
# Pelvis yaw reward: pelvis faces target direction
pelvis_yaw_reward = tolerance(dot(pelvis_forward, target_dir), bounds=(0.9, 1.0), margin=0.3, sigmoid="linear")
# Pelvis level reward: pelvis remains level
pelvis_level_reward = tolerance(pelvis_up, bounds=(0.9, 1.0), margin=0.3, sigmoid="linear")
# Feet height reward: feet stay close to ground
feet_height_reward = tolerance(max_foot_height, bounds=(0.0, 0.3), margin=0.5, sigmoid="quadratic")
# Gait reward = product of all heading and posture rewards
gait_reward = torso_heading_reward * head_heading_reward * pelvis_yaw_reward * pelvis_level_reward * feet_height_reward
```
### Total Reward
```python
total_reward = posture_reward * speed_reward * energy_reward * gait_reward
```
---
## Initial State
- **Robot Position**: Torso initial height is 1.33 meters (95% of standard stand height)
- **Robot Orientation**: Torso remains upright, quaternion set to (1.0, 0.0, 0.0, 0.0)
- **Joint Angles**: Randomly initialized within joint limits
- Torso/hip base joints: Randomized in small range (±15 degrees)
- Leg joints: Symmetrically initialized, ensuring left and right legs are independently randomized, knees initially bent
- Arm joints: Symmetrically initialized, using middle 80% of joint limit ranges
- **Initial Velocities**: All joint velocities and linear velocities initialized to small random values near zero (-0.01 to 0.01)
- **Initial Controls**: All actuator controls initialized to small random values near zero (-0.02 to 0.02)
## Episode Termination Conditions
- Robot state observations contain abnormal values (NaN or Inf)
- Head too low: Head height below 50% of standard stand height (0.7 meters)
- Torso too tilted: Torso vertical component less than 0.2 (severe torso tilt)
- Extreme velocity: Absolute value of any joint velocity exceeds 200.0 rad/s or m/s
- Maximum episode duration: 25 seconds
---
## Usage Guide
### 1. Environment Preview
```bash
uv run scripts/view.py --env dm-humanoid-stand
uv run scripts/view.py --env dm-humanoid-walk
uv run scripts/view.py --env dm-humanoid-run
```
### 2. Start Training
```bash
uv run scripts/train.py --env dm-humanoid-stand
uv run scripts/train.py --env dm-humanoid-walk
uv run scripts/train.py --env dm-humanoid-run
```
### 3. View Training Progress
```bash
uv run tensorboard --logdir runs/dm-humanoid-walk
```
### 4. Test Training Results
```bash
uv run scripts/play.py --env dm-humanoid-stand
uv run scripts/play.py --env dm-humanoid-walk
uv run scripts/play.py --env dm-humanoid-run
```
---
## Expected Training Results
### Standing Task (dm-humanoid-stand)
1. Head height maintained in 1.3-1.5m range
2. Torso upright angle deviation less than 15 degrees
3. Able to stand stably without falling
4. Speed near zero, no significant movement
### Walking Task (dm-humanoid-walk)
1. Actual walking speed close to 1.0 m/s
2. Coordinated gait, no obvious falls
3. Able to walk continuously and stably
4. Torso and head facing target direction
### Running Task (dm-humanoid-run)
1. Running speed reaches 5.0-10.0 m/s
2. Flight phase appears (both feet off ground simultaneously)
3. Coordinated and stable gait
4. Able to maintain high-speed running posture

View File

@@ -0,0 +1,136 @@
# Pendulum
Pendulum is a single-joint swing-up and balance task. The goal is to swing the pole up and keep it inverted using one motor torque.
```{video} /_static/videos/pendulum.mp4
:poster: _static/images/poster/pendulum.jpg
:nocontrols:
:autoplay:
:playsinline:
:muted:
:loop:
:width: 100%
```
---
## Task Description
A single-link pendulum with one hinge joint is driven by a single motor (configurable gear). The motors torque rotates the rod in a plane, enabling swing-up from arbitrary initial angles, inverted balance, and maintenance. Torque is limited by the actuator ctrlrange; by modulating its magnitude and direction, the policy must accumulate energy to swing up and stabilize near the inverted position while damping angular-velocity-induced oscillations.
## Action Space
| Item | Details |
| ------------- | ------------------------------- |
| **Type** | `Box(-1.0, 1.0, (1,), float32)` |
| **Dimension** | 1 |
---
## Observation Space
| Item | Details |
| ------------- | ------------------------------- |
| **Type** | `Box(-inf, inf, (3,), float32)` |
| **Dimension** | 3 |
Order: `cos(theta), sin(theta), angular velocity`.
---
## Reward Function Design
- Upright reward: encourages angle near π (inverted)
- Energy shaping: target energy near inverted position
- Penalties: `ang_vel^2`, `ctrl^2`, `(ctrl - prev_ctrl)^2` to reduce oscillation and aggressive actuation
---
## Initial State
- Angle randomized in `[-pi, pi]`
- Angular velocity small random noise (if configured)
- Control history (`prev_ctrl`) reset to zero
## Episode Termination Conditions
- No fall/angle termination; only NaN check
- Episode length limited by `max_episode_seconds`
---
### 1. Environment Preview
```bash
uv run scripts/view.py --env pendulum
```
### 2. Start Training
```bash
# Train with default parameters
uv run scripts/train.py --env pendulum
# Customize parallel environments
uv run scripts/train.py --env pendulum --num-envs 1024
# Enable rendering during training
uv run scripts/train.py --env pendulum --render
```
### 3. View Training Progress
```bash
uv run tensorboard --logdir runs/pendulum
```
### 4. Test Training Results
```bash
# Auto-discover best policy (recommended)
uv run scripts/play.py --env pendulum
# Manually specify a policy file
uv run scripts/play.py --env pendulum --policy runs/pendulum/nn/best_policy.pickle
```
> **Tip**: Policies are auto-selected from `runs/pendulum/`. You can override with `--policy`.
---
## Configuration Parameters
### Environment Configuration
```python
@dataclass
class PendulumEnvCfg(EnvCfg):
model_file: str = ".../pendulum.xml" # MJCF model (gear=5)
max_episode_seconds: float = 20.0
sim_dt: float = 0.0125
ctrl_dt: float = 0.025
```
### Training Configuration (PPO example)
```python
@rlcfg("pendulum")
@dataclass
class PendulumPPO(PPOCfg):
seed: int = 42
max_env_steps: int = 10_000_000
num_envs: int = 1024
learning_rate: float = 3e-4
rollouts: int = 32
learning_epochs: int = 5
mini_batches: int = 4
policy_hidden_layer_sizes: tuple[int, ...] = (64, 64)
value_hidden_layer_sizes: tuple[int, ...] = (64, 64)
```
---
## Expected Training Results
1. Pendulum can swing up and stay near inverted
2. Oscillation around upright is reduced by angular-velocity and control-change penalties

View File

@@ -0,0 +1,381 @@
# Shadow Hand Cube Repose
## Overview
This document describes in detail the Shadow Hand dexterous manipulation cube reorientation task environment. This is a classic benchmark test in the field of robotic manipulation, requiring the robot to reorient a cube in-hand to match a randomly sampled target orientation.
```{video} /_static/videos/shadow_hand_repose.mp4
:poster: _static/images/poster/shadow_hand_repose.jpg
:nocontrols:
:autoplay:
:playsinline:
:muted:
:loop:
:width: 100%
```
---
## Environment Description
The Shadow Hand cube reorientation task is built based on the realistic Shadow Hand 24-DOF dexterous hand, designed to train robots to rotate a cube in-hand to a target pose through fine multi-finger coordination.
### Robot Structure
Shadow Hand is a high-fidelity 24-DOF dexterous hand composed of the following main parts:
- **Palm**: The base structure of the hand, fixed to the base
- **5 Fingers**:
- **Thumb**: 5 degrees of freedom, including CMC rotation, MCP, IP joints
- **Index**: 4 degrees of freedom, including MCP, PIP, DIP joints
- **Middle**: 4 degrees of freedom, including MCP, PIP, DIP joints
- **Ring**: 4 degrees of freedom, including MCP, PIP, DIP joints
- **Little**: 4 degrees of freedom, including MCP, PIP, DIP joints
- **Actuator Configuration**: 20 actuated joints (4 coupled joints driven by other joints)
### Finger Joint Details
Each finger (except thumb) contains:
- **MCP Joint**: Metacarpophalangeal joint, 2 degrees of freedom (abduction/adduction + flexion)
- **PIP Joint**: Proximal interphalangeal joint, 1 degree of freedom (flexion)
- **DIP Joint**: Distal interphalangeal joint, 1 degree of freedom (flexion)
Thumb contains:
- **CMC Joint**: Carpometacarpal joint, 2 degrees of freedom
- **MCP Joint**: Metacarpophalangeal joint, 1 degree of freedom
- **IP Joint**: Interphalangeal joint, 1 degree of freedom
### Environment Objects
- **Cube**: 50mm \* 50mm \* 50mm cube
- Mass: approximately 0.028 kg
- Friction coefficient: 1.2
- Initial position: above palm center `(0.33, 0.00, 0.295)` meters
- **Target Visualization**: Semi-transparent target pose indicator (mocap body)
### Task Goals
The robot needs to complete the following operation goals:
1. **Maintain Grasp**: Maintain stable grip of the cube in-hand
2. **Perceive Goal**: Observe target pose (visualization indicator)
3. **Fine Manipulation**: Rotate cube through multi-finger coordination
4. **Pose Alignment**: Rotate cube pose to target pose (tolerance <20>0.1 radian)
---
## Action Space
Action space is `Box(-1, 1, (20,), float32)`, representing position control commands applied to 20 actuated joints (normalized).
### Control Mode
The environment uses position control mode, actions are converted to joint target positions through:
```
1. Scale from [-1, 1] to actuator control range
2. Optional: action smoothing (moving average filter)
3. Clip to joint limits
4. Apply to simulator actuators
```
### Action Processing Flow
```python
# 1. Scale to actuator limits
targets = scale(actions, lower_limits, upper_limits)
# 2. Action smoothing (optional)
if act_moving_average < 1.0:
targets = α * targets + (1-α) * prev_actions
# 3. Clip to limits
targets = clip(targets, lower_limits, upper_limits)
# 4. Apply control
actuator_ctrls = targets
```
### Action Dimension Details
| Index | Finger | Joint | DOF | Description |
| ----- | ------ | ----- | --- | ------------- |
| 0-4 | Thumb | J0-J4 | 5 | CMC, MCP, IP |
| 5-8 | Index | J0-J3 | 4 | MCP, PIP, DIP |
| 9-12 | Middle | J0-J3 | 4 | MCP, PIP, DIP |
| 13-16 | Ring | J0-J3 | 4 | MCP, PIP, DIP |
| 17-20 | Little | J0-J3 | 4 | MCP, PIP, DIP |
---
## Observation Space
Observation space is `Box(-inf, inf, (157,), float32)`, containing robot proprioceptive information, cube state, target state, and fingertip state.
### Observation Components
Observation vector consists of the following parts (in order):
#### 1. Hand Joint State (48 dimensions)
- **Joint Position (24 dims)**: Unscaled raw joint angles
- **Joint Velocity (24 dims)**: Joint angular velocities scaled by 0.2
#### 2. Cube State (17 dimensions)
- **Position (3 dims)**: Cube position in world coordinates `(x, y, z)`
- **Orientation (4 dims)**: Quaternion `(x, y, z, w)`
- **Linear Velocity (3 dims)**: Cube linear velocity
- **Angular Velocity (3 dims)**: Angular velocity scaled by 0.2
- **Normalization Factor**: Velocity observations multiplied by `vel_obs_scale = 0.2`
#### 3. Goal State (11 dimensions)
- **Goal Position (3 dims)**: Fixed at `(0.33, 0.00, 0.295)`
- **Goal Orientation (4 dims)**: Randomly sampled target quaternion
- **Relative Rotation (4 dims)**: Relative quaternion from cube to goal
#### 4. Fingertip State (65 dimensions)
State of 5 fingertips, 13 dimensions per fingertip:
- **Position (3 dims)**: Fingertip position in Cartesian space
- **Orientation (4 dims)**: Fingertip quaternion
- **Velocity (6 dims)**: Linear and angular velocities
**Fingertip Link Names**:
- `rh_ffdistal`: Index fingertip
- `rh_mfdistal`: Middle fingertip
- `rh_rfdistal`: Ring fingertip
- `rh_lfdistal`: Little fingertip
- `rh_thdistal`: Thumb fingertip
#### 5. Action History (20 dimensions)
- Previous action values, for temporal context in the policy
### Observation Details
| Index | Observation Content | Dimension | Range | Unit |
| ------- | -------------------------------- | --------- | ------------ | ----- |
| 0-23 | Hand joint position (unscaled) | 24 | Joint limits | rad |
| 24-47 | Hand joint velocity (0.2) | 24 | ±π/2 | rad/s |
| 48-50 | Cube position | 3 | Real | m |
| 51-54 | Cube orientation (quat w,x,y,z) | 4 | Unit norm | - |
| 55-57 | Cube linear velocity | 3 | Real | m/s |
| 58-60 | Cube angular velocity (0.2) | 3 | Real | rad/s |
| 61-63 | Goal position | 3 | Fixed | m |
| 64-67 | Goal orientation (quat w,x,y,z) | 4 | Unit norm | - |
| 68-71 | Relative rotation (quat w,x,y,z) | 4 | Unit norm | - |
| 72-136 | Fingertip state (5\*13) | 65 | - | - |
| 137-156 | Previous action | 20 | [-1, 1] | - |
---
## Reward Function
The reward function uses a composite design with multiple reward and penalty terms.
### Main Reward Terms
1. **Rotation Alignment Reward** (core objective)
```
rot_reward = rot_reward_scale / (|rot_dist| + rot_eps)
```
- **Scaling factor**: `1.0`
- **Epsilon**: `0.1`
- **Rotation distance calculation**: Using quaternion rotation distance formula
- **Incentive**: Reward grows inversely as cube orientation approaches target
2. **Position Distance Penalty**
```
dist_reward = dist_reward_scale <20> goal_dist
```
- **Scaling factor**: `-10.0`
- **Distance calculation**: Euclidean distance from cube to goal position
- **Incentive**: Prevent cube from dropping, keep near target position
3. **Action Regularization Penalty**
```
action_penalty = action_penalty_scale <20> ||actions||<7C>
```
- **Scaling factor**: `-0.0002`
- **Purpose**: Encourage smooth, energy-efficient motion
### Conditional Rewards
4. **Success Reward**
```
if |rot_dist| d success_tolerance:
reward += reach_goal_bonus
```
- **Reward value**: `2.0`
- **Tolerance**: `0.1` radian (approximately 5.7<EFBFBD>)
- **Purpose**: Sparse reward for achieving goal alignment
5. **Drop Penalty**
```
if goal_dist e fall_dist:
reward += fall_penalty
terminated = True
```
- **Penalty value**: `0.0` (termination only, no additional penalty)
- **Distance threshold**: `0.24` meters
- **Purpose**: Terminate episode when cube is dropped
---
## Initial State
### Hand Initialization
**Position Initialization:**
The palm is fixed in the world coordinate system, position determined by the model file.
**Joint Angle Initialization:**
- Use model default joint positions
- Add uniform random noise: `[-0.2, 0.2]` radians
- Range: All 24 hand degrees of freedom
**Velocity Initialization:**
All joint velocities are initialized to zero.
### Cube Initialization
**Position Initialization:**
- Fixed position: `(0.33, 0.00, 0.295)` above palm center
- Add uniform random noise: `[-0.01, 0.01]` (<28>1cm)
**Orientation Initialization:**
- Use Shoemake method to generate uniformly distributed random quaternions
- Ensure uniform sampling on SO(3) space
**Velocity Initialization:**
All linear and angular velocities are initialized to zero.
### Goal Initialization
**Position Initialization:**
- Fixed position: `(0.33, 0.00, 0.295)` (same as cube initial position)
**Orientation Initialization:**
- Use Shoemake method to generate uniformly distributed random target quaternions
- Resampled each reset
---
## Termination Conditions
Episodes terminate under the following conditions:
1. **Drop Termination**: Cube distance from goal position e `fall_dist` (0.24m)
2. **Timeout Termination**: Reaching `max_episode_steps` (default 1000 steps)
3. **NaN Protection**: Detecting rotation distance or position distance as NaN
### Success Holding Mechanism
The environment uses a consecutive success counter:
- When rotation tolerance is satisfied, counter increments
- When `max_consecutive_successes` (50) is reached, trigger success termination and reset goal
- Rotation tolerance: `0.1` radian
---
## Usage
### Training
```bash
uv run scripts/train.py --env shadow-hand-repose
```
### Policy Evaluation
```bash
uv run scripts/play.py --env shadow-hand-repose
```
### Environment Visualization
```bash
uv run scripts/view.py --env shadow-hand-repose
```
### TensorBoard
```bash
uv run tensorboard --logdir runs/shadow-hand-repose
```
---
## Configuration Parameters
### Environment Parameters
| Parameter | Default | Description |
| --------------------- | ------- | -------------------------- |
| `max_episode_seconds` | 10.0 | Maximum episode length (s) |
| `ctrl_dt` | 0.01 | Control timestep (s) |
| `max_episode_steps` | 1000 | Maximum episode steps |
| `num_hand_dofs` | 24 | Total hand DOFs |
| `num_actuators` | 20 | Number of actuated joints |
### Reward Parameters
| Parameter | Default | Description |
| ---------------------- | ------- | ------------------------- |
| `dist_reward_scale` | -10.0 | Position distance reward |
| `rot_reward_scale` | 1.0 | Rotation alignment reward |
| `rot_eps` | 0.1 | Rotation reward epsilon |
| `action_penalty_scale` | -0.0002 | Action regularization |
| `success_tolerance` | 0.1 | Success tolerance (rad) |
| `reach_goal_bonus` | 2.0 | Success reward |
| `fall_dist` | 0.24 | Drop distance threshold |
| `fall_penalty` | 0.0 | Drop penalty |
### Reset Noise Parameters
| Parameter | Default | Description |
| ---------------------- | ------- | ---------------------------- |
| `reset_position_noise` | 0.01 | Cube position noise (m) |
| `reset_dof_pos_noise` | 0.2 | Joint position noise (rad) |
| `reset_dof_vel_noise` | 0.0 | Joint velocity noise (rad/s) |
### Observation Scaling Parameters
| Parameter | Default | Description |
| --------------- | ------- | -------------------------- |
| `vel_obs_scale` | 0.2 | Velocity observation scale |
---
## References
This environment is based on the following classic works:
- **OpenAI Dactyl** (2018): First successful in-hand manipulation sim-to-real transfer
- **Isaac Gym** (2021): High-performance GPU-accelerated physics simulation
- **Isaac Lab** (2023): Modular robot learning framework

View File

@@ -0,0 +1,189 @@
# Container Deployment
This document describes how to deploy MotrixLab using Docker containers to simplify environment configuration and enable rapid deployment.
## Prerequisites
- **Docker** and **Docker Compose**: [Installation Docs](https://docs.docker.com/engine/install/)
- **NVIDIA Container Toolkit** (for GPU support): [Installation Docs](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html#installation)
- **NVIDIA GPU**: Graphics card supporting CUDA 12.8
## Quick Start
### 1. Clone the Repository
```bash
git clone https://github.com/Motphys/MotrixLab.git
cd MotrixLab/docker
```
### 2. Start with Docker Compose
We provide a complete `docker-compose.yml` configuration file that supports one-click startup of training and TensorBoard visualization services.
```bash
# Start training and TensorBoard services
docker compose up -d
```
This will start the following services:
- **motrixlab-training**: Training container that executes reinforcement learning training tasks
- **motrixlab-tensorboard**: TensorBoard visualization service accessible via browser at http://localhost:6006
### 3. Configure Training Parameters
You can customize training configurations through environment variables:
```bash
# Set training backend (jax or torch)
export MOTRIX_TRAIN_BACKEND=jax
# Set number of parallel environments
export MOTRIX_NUM_ENVS=2048
# Set training environment name
export MOTRIX_ENV=cartpole
# Start services
docker compose up -d
```
| Environment Variable | Default | Description |
| :--------------------- | :--------- | :--------------------------------- |
| `MOTRIX_TRAIN_BACKEND` | `jax` | Training backend: `jax` or `torch` |
| `MOTRIX_NUM_ENVS` | `2048` | Number of parallel environments |
| `MOTRIX_ENV` | `cartpole` | Training environment name |
### 4. Monitor Training Progress
Training logs are automatically saved to the Docker Volume `motrixlab-data`. You can view them through:
```bash
# View training container logs
docker logs -f motrixlab-training
# Access TensorBoard
# Open in browser: http://localhost:6006
```
### 5. Stop Services
```bash
# Stop all services
docker compose down
# Stop services and remove data volumes
docker compose down -v
```
## Advanced Usage
### Building Docker Images
If you need to customize the image, you can build from source:
```bash
# Execute from project root directory
cd docker
docker build -t motphys/motrixlab:latest .
```
### Running Single Container
If you only want to run the training container without using Docker Compose:
```bash
docker run --gpus all \
-v $(pwd)/runs:/root/motrixlab/runs \
motphys/motrixlab:latest \
scripts/train.py --train-backend jax --num-envs 2048 --env cartpole
```
### Persisting Training Results
The default configuration uses the Docker Volume `motrixlab-data` to save training results. You can mount it to a host directory:
```bash
# Modify volumes configuration in docker-compose.yml
volumes:
- ./runs:/root/motrixlab/runs
```
## Image Details
Our Docker image is based on {bdg-primary-line}`NVIDIA CUDA 12.8.1` runtime environment and comes pre-installed with the following components:
- **UV Package Manager**: Fast, reliable dependency management
- **MotrixLab**: Complete reinforcement learning training framework
- **SKRL**: Reinforcement learning library supporting both JAX and PyTorch backends
- **TensorBoard**: Training process visualization tool
- **MotrixSim**: High-performance physics simulation engine
Image layer build process:
1. **Base Environment**: NVIDIA CUDA 12.8.1 Runtime + Ubuntu 24.04
2. **System Dependencies**: Install UV package manager and necessary system tools
3. **Python Dependencies**: Use UV cache mechanism for fast Python package installation
4. **Project Code**: Copy MotrixLab source code and complete dependency installation
## Troubleshooting
### GPU Not Available
If the container cannot access the GPU:
```bash
# Check NVIDIA Docker Runtime
docker run --rm --gpus all nvidia/cuda:12.8.1-base-ubuntu24.04 nvidia-smi
# Verify NVIDIA Container Toolkit is correctly installed
which nvidia-container-cli
```
### Insufficient Storage Space
Clean Docker cache and unused images:
```bash
# Clean build cache
docker builder prune
# Delete unused images
docker image prune -a
# Clean all unused resources
docker system prune -a
```
## Performance Optimization
### Accelerate Builds with UV Cache
The Dockerfile uses UV's cache mount feature, which can significantly speed up rebuilds:
```bash
# Rebuild using UV cache
docker build --cache-from motphys/motrixlab:latest -t motphys/motrixlab:latest .
```
### GPU Resource Allocation
You can specify the number of GPUs to use in `docker-compose.yml`:
```yaml
deploy:
resources:
reservations:
devices:
- driver: nvidia
device_ids: ["0", "1"] # Use GPU 0 and 1
capabilities: [gpu]
```
## Next Steps
- Check out the [Quick Start Tutorial](hello_motrixlab.md) to learn basic MotrixLab usage
- Read [Training Examples](../demo/cartpole.md) for more training tasks
- Explore [Basic Framework](../tutorial/basic_frame.md) for in-depth understanding of the framework architecture

View File

@@ -5,6 +5,7 @@
:maxdepth: 1
getting_started/installation
getting_started/container_deployment
getting_started/hello_motrixlab
```
@@ -25,16 +26,22 @@ tutorial/training_and_result
:caption: Training Examples
:maxdepth: 1
demo/acrobot
demo/cartpole
demo/pendulum
demo/bounce_ball
demo/dm_walker
demo/dm_cheetah
demo/dm_hopper
demo/dm_reacher
demo/dm_finger
demo/dm_humanoid
demo/bring_ball
demo/locomotion_unitree_go1
demo/locomotion_unitree_go1_rough_terrain
demo/anymal_c
demo/franka_lift_cube
demo/franka_open_cabinet
demo/shadow_hand_repose
```

View File

@@ -161,6 +161,20 @@ MotrixLab 是一个为机器人训练设计的机器学习通用架构。它基
:::{grid-item-card}
```{video} _static/videos/dm_humanoid_run.mp4
:poster: _static/images/poster/dm_humanoid_run.jpg
:nocontrols:
:autoplay:
:playsinline:
:muted:
:loop:
:width: 100%
```
:::
:::{grid-item-card}
```{video} _static/videos/bounce_ball.mp4
:poster: _static/images/poster/bounce_ball.jpg
:nocontrols:

View File

@@ -0,0 +1,154 @@
# Acrobot 双连杆
Acrobot 是一个双连杆摆动和平衡任务。目标是使用一个电机扭矩摆动双臂并到达目标位置。
```{video} /_static/videos/acrobot.mp4
:poster: _static/images/poster/acrobot.jpg
:nocontrols:
:autoplay:
:playsinline:
:muted:
:loop:
:width: 100%
```
---
## 任务描述
双连杆 Acrobot 由一个铰链关节驱动,该关节由单个电机控制。电机安装在肘关节处,这是系统中唯一的驱动关节。电机的扭矩使连杆在平面内旋转,实现从任意初始角度摆起并到达目标位置。扭矩受限于执行器的 ctrlrange通过调节其大小和方向策略必须积累能量以摆起并到达目标同时保持稳定性。
## 动作空间
| 项目 | 详情 |
| -------- | ------------------------------- |
| **类型** | `Box(-1.0, 1.0, (1,), float32)` |
| **维度** | 1 |
---
## 观察空间
| 项目 | 详情 |
| -------- | ------------------------------- |
| **类型** | `Box(-inf, inf, (6,), float32)` |
| **维度** | 6 |
顺序:`upper_arm_horizontal, lower_arm_horizontal, upper_arm_vertical, lower_arm_vertical, shoulder_velocity, elbow_velocity`。
---
## 奖励函数设计
- 基础稀疏奖励:鼓励末端进入目标区域(半径 = 0.2
- 持续奖励:在目标区域内每步提供 0.1 的奖励
- 距离奖励0.3 \* (1.0 - clip(distance / 2.0, 0, 1.0)) 鼓励向目标移动
- 速度惩罚0.01 \* max(0, velocity_magnitude - 2.0) 惩罚过高的速度
---
## 初始状态
- 肩关节角度随机于 `[-pi, pi]`
- 肘关节角度随机于 `[-pi, pi]`
- 角速度初始化为零
## Episode 终止条件
- Episode 长度由 `max_episode_seconds` 限制
- 对观察值进行 NaN 检查
---
### 1. 环境预览
```bash
uv run scripts/view.py --env acrobot
```
### 2. 开始训练
```bash
# 使用默认参数训练
uv run scripts/train.py --env acrobot
# 自定义并行环境数
uv run scripts/train.py --env acrobot --num-envs 1024
# 开启训练时渲染
uv run scripts/train.py --env acrobot --render
```
### 3. 查看训练进度
```bash
uv run tensorboard --logdir runs/acrobot
```
### 4. 测试训练结果
```bash
# 自动发现最佳策略(推荐)
uv run scripts/play.py --env acrobot
# 手动指定策略文件
uv run scripts/play.py --env acrobot --policy runs/acrobot/nn/best_policy.pickle
```
> **提示**:策略会从 `runs/acrobot/` 中自动选择。你可以使用 `--policy` 参数覆盖。
---
## 配置参数
### 环境配置
```python
@dataclass
class AcrobotEnvCfg(EnvCfg):
model_file: str = ".../acrobot.xml" # MJCF 模型
max_episode_seconds: float = 10.0
sim_dt: float = 0.01
ctrl_dt: float = 0.02
reset_noise_scale: float = 0.1
render_spacing: float = 2.0
```
### 训练配置PPO 示例)
```python
@rlcfg("acrobot", backend="jax")
@dataclass
class AcrobotPPO(PPOCfg):
max_env_steps: int = 60_000_000
check_point_interval: int = 500
# 覆盖 PPO 配置
policy_hidden_layer_sizes: tuple[int, ...] = (32, 32)
value_hidden_layer_sizes: tuple[int, ...] = (32, 32)
rollouts: int = 64
learning_epochs: int = 5
mini_batches: int = 8
learning_rate: float = 3e-4
grad_norm_clip: float = 0.1
clip_predicted_values: bool = False
value_clip: float = 10.0
entropy_loss_scale: float = 0.1
learning_rate_scheduler_kl_threshold: float = 0.02
discount_factor: float = 0.995
lambda_param: float = 0.97
ratio_clip: float = 0.2
value_loss_scale: float = 0.5
random_timesteps: int = 0
learning_starts: int = 0
kl_threshold: float = 0.03
```
---
## 预期训练结果
1. Acrobot 能够摆动双臂到达目标位置
2. 末端能够稳定地停留在目标区域内
3. 过高的震荡通过速度惩罚得到减少
4. 策略能够以平滑的动作高效地接近目标

View File

@@ -1,4 +1,4 @@
# ANYmal-C Locomotion
# ANYmal-C 导航
## 概述

View File

@@ -42,46 +42,323 @@ Bounce Ball 是一个单臂机器人操作任务,使用 6 自由度的配天 A
| 项目 | 详细信息 |
| -------- | -------------------------------- |
| **类型** | `Box(-inf, inf, (25,), float32)` |
| **维度** | 25 |
| **类型** | `Box(-inf, inf, (29,), float32)` |
| **维度** | 29 |
观察空间由以下部分组成(按顺序):
| 部分 | 内容说明 | 维度 | 备注 |
| ----------- | -------------------- | ---- | --------------------------------------------------------------- |
| ----------------- | -------------------- | ---- | --------------------------------------------------------------- |
| **dof_pos** | 各关节自由度位置信息 | 13 | 前 6 个为机械臂关节,后 7 个为球的自由关节3 位置 + 4 四元数) |
| **dof_vel** | 各关节自由度速度信息 | 12 | 速度为位置导数 |
| **paddle_pos** | 挡板位置信息 | 3 | 挡板中心的 x, y, z 坐标 |
| **target_height** | 目标高度 | 1 | 当前环境的目标高度 |
| 序号 | 观察量 | 最小值 | 最大值 | XML 名称 | 类型 (单位) |
| ----- | -------------- | ------ | ------ | ----------- | ---------------- |
| ----- | -------------------------- | ------ | ------ | --------- | ----------------- |
| 0-5 | 机械臂关节角度 | -Inf | Inf | Joint1-6 | 角度 (rad) |
| 6 | 球 x 坐标 | -Inf | Inf | ball_x | 位置 (m) |
| 7 | 球 y 坐标 | -Inf | Inf | ball_y | 位置 (m) |
| 8 | 球 z 坐标 | -Inf | Inf | ball_z | 位置 (m) |
| 9-12 | 球姿态四元数 | -Inf | Inf | ball_qw/xyz | 四元数 (w,x,y,z) |
| 13-24 | 关节和球速度 | -Inf | Inf | - | 速度/角速度 |
| 6-8 | 球位置 [x, y, z] | -Inf | Inf | ball_link | 位置 (m) |
| 9-12 | 球姿态四元数 [w,x,y,z] | -Inf | Inf | ball_link | 四元数 |
| 13-18 | 机械臂关节角速度 | -Inf | Inf | Joint1-6 | 角速度 (rad/s) |
| 19-24 | 球速度 [vx,vy,vz,wx,wy,wz] | -Inf | Inf | ball_link | 速度 (m/s, rad/s) |
| 25-27 | 挡板位置 [x, y, z] | -Inf | Inf | blocker | 位置 (m) |
| 28 | 目标高度 | -Inf | Inf | - | 位置 (m) |
---
## 奖励函数设计
## 奖励函数
奖励函数由以下几个部分组成:
奖励函数采用复合设计,包含多个奖励和惩罚项,引导机器人学习稳定的颠球策略。所有奖励参数可通过配置文件调整。
```python
# 位置控制奖励:保持球在挡板中心上方
# 受控向上速度奖励:在球位置良好时奖励适中的向上速度
# 高度精度奖励:球接近目标高度
# 连续弹跳奖励:奖励连续成功的弹跳
# 总奖励 = 各项加权组合
```
### 主要奖励项
---
#### 1. 水平位置奖励
## 初始状态
**设计意义**:这是最核心的奖励项,确保球始终保持在挡板正上方。通过垂直距离加权机制,当球接近挡板时(即将击打时刻)对水平位置的要求更严格,引导策略在关键时刻精确对齐。
- **机械臂初始位置**[0, 40, 110, 0, -60, 0] 度,带有随机噪声
- **球的初始位置**:挡板中心上方,带有随机噪声
- **球的初始速度**[0.0, 0.0, 0.0] m/s
**计算公式**
$$
\begin{aligned}
\text{err}_{xy} &= \sqrt{(x_{ball} - x_{target})^2 + (y_{ball} - y_{target})^2} &&\text{(水平位置误差)} \\
d_{vert} &= |z_{ball} - z_{paddle}| &&\text{(垂直距离)} \\
w_{vert} &= e^{-d_{vert} / \sigma_{vert}} &&\text{(垂直距离权重)} \\
\sigma_{pos} &= \sigma_{base} \times (1.0 + k_{weight} \times w_{vert}) &&\text{(自适应尺度)} \\
r_{pos} &= e^{-\frac{\text{err}_{xy}^2}{2\sigma_{pos}^2}} &&\text{(高斯奖励)} \\
\\
\text{其中:} \quad &\sigma_{vert} = 0.15 \text{ m} &&\text{(垂直距离尺度)} \\
&\sigma_{base} = 0.1 \text{ m} &&\text{(基础水平尺度)} \\
&k_{weight} = 3.0 &&\text{(权重因子)} \\
&x_{target} = 0.58856 \text{ m}, \, y_{target} = 0.0 \text{ m} &&\text{(目标位置)}
\end{aligned}
$$
**权重**2.0
#### 2. 位置偏离惩罚
**设计意义**:对严重偏离目标位置的情况施加强惩罚,防止球飞出控制范围。使用 sigmoid 函数实现平滑过渡,避免奖励函数不连续。
**计算公式**
$$
r_{out} = -\frac{2.0}{1 + e^{-(\text{err}_{xy} - 0.05) / 0.03}} \qquad \text{sigmoid 惩罚)}
$$
**权重**1.0
#### 3. 速度匹配奖励
**设计意义**基于抛体运动物理规律激励球的运动轨迹能够在目标高度时具有期望的速度0.5 m/s。这确保球不会过快或过慢地通过目标高度有利于稳定控制。
**计算公式**
$$
\begin{aligned}
\Delta h &= h_{target} - z_{ball} &&\text{(高度差)} \\
v_{desired} &= 0.5 \text{ m/s} &&\text{(期望速度)} \\
\\
\text{情况1}&\text{球向上运动且低于目标高度} \\
v_{z,up}^2 &= v_z^2 - 2g\Delta h &&\text{(能量守恒)} \\
v_{at\_target,up} &= \sqrt{\max(0, v_{z,up}^2)} &&\text{(向上到达速度)} \\
\\
\text{情况2}&\text{球向下运动且高于目标高度} \\
v_{z,down}^2 &= v_z^2 + 2g|\Delta h| &&\text{(能量守恒)} \\
v_{at\_target,down} &= -\sqrt{\max(0, v_{z,down}^2)} &&\text{(向下到达速度)} \\
\\
\text{情况3}&\text{球接近目标高度(} |\Delta h| < 0.05 \text{ m} \\
v_{at\_target,near} &= v_z &&\text{(当前速度)} \\
\\
\text{平滑组合:}& \\
\sigma_{up} &= \frac{1}{1 + e^{-v_z / 0.2}} &&\text{(向上运动权重)} \\
\sigma_{below} &= \frac{1}{1 + e^{-\Delta h / 0.02}} &&\text{(低于目标权重)} \\
\sigma_{down} &= 1 - \sigma_{up} &&\text{(向下运动权重)} \\
\sigma_{above} &= 1 - \sigma_{below} &&\text{(高于目标权重)} \\
w_{near} &= e^{-\frac{\Delta h^2}{2 \times 0.01^2}} &&\text{(接近目标权重)} \\
\\
v_{at\_target} &= v_{at\_target,up} \cdot \sigma_{up} \cdot \sigma_{below} \\
&\quad + v_{at\_target,down} \cdot \sigma_{down} \cdot \sigma_{above} \\
&\quad + v_{at\_target,near} \cdot w_{near} &&\text{(加权组合)} \\
\\
\text{err}_{vel} &= |v_{at\_target} - v_{desired}| &&\text{(速度误差)} \\
r_{vel} &= e^{-\frac{\text{err}_{vel}^2}{2 \times 0.8^2}} &&\text{(高斯奖励)}
\end{aligned}
$$
**权重**2.0
#### 4. 高度奖励
**设计意义**直接激励球接近目标高度这是任务的核心目标之一。较高的权重4.5确保策略优先考虑高度控制。目标高度在每个环境中随机采样0.3-0.6 m提升策略的泛化能力。
**计算公式**
$$
\begin{aligned}
\text{err}_h &= |z_{ball} - h_{target}| &&\text{(高度误差)} \\
r_h &= e^{-\frac{\text{err}_h^2}{2 \times 0.15^2}} &&\text{(高斯奖励)}
\end{aligned}
$$
**权重**4.5
#### 5. 高度进步奖励
**设计意义**:激励球达到更高位置,帮助策略在训练初期快速学会向上击打球,避免陷入"不击打"的局部最优。
**计算公式**
$$
r_{progress} = \max(0, z_{ball} - 0.2) \times 2.0 \qquad \text{(线性奖励)}
$$
**权重**1.0
#### 6. 受控向上速度奖励
**设计意义**:只有当球水平位置良好时才奖励向上速度,避免"乱打"行为。理想速度根据物理公式计算,确保球能恰好达到目标高度。这个奖励引导策略学习精确的击打力度。
**计算公式**
$$
\begin{aligned}
q_{pos} &= e^{-\frac{\text{err}_{xy}^2}{2 \times 0.02^2}} &&\text{(位置质量)} \\
v_{ideal} &= \sqrt{2g \times \max(0, \Delta h)} &&\text{(理想发射速度)} \\
v_{ideal} &\in [0.5, 3.0] \text{ m/s} &&\text{(限制范围)} \\
q_{vel} &= e^{-\frac{(v_z - v_{ideal})^2}{2 \times 0.5^2}} &&\text{(速度质量)} \\
\sigma_{up} &= \frac{1}{1 + e^{-v_z / 0.1}} &&\text{(向上掩码)} \\
r_{controlled} &= q_{pos} \times q_{vel} \times \sigma_{up} \times \text{clip}(v_z, 0, 1.5) &&\text{(组合奖励)}
\end{aligned}
$$
**权重**1.5
#### 7. 连续弹跳奖励
**设计意义**:激励多次连续成功弹跳,引导策略学习稳定的长期控制。使用对数函数避免奖励无限增长,同时要求球位置良好才给予奖励。
**计算公式**
$$
\begin{aligned}
q_{bounce} &= e^{-\frac{\text{err}_{xy}^2}{2 \times 0.05^2}} &&\text{(弹跳位置质量)} \\
r_{bounce\_log} &= 0.5 \times \log(n_{bounces} + 1) &&\text{(对数奖励)} \\
r_{bounce} &= r_{bounce\_log} \times q_{bounce} \times \mathbb{1}_{n_{bounces} > 0} &&\text{(条件奖励)}
\end{aligned}
$$
**权重**0.8
#### 8. 高弹跳次数奖励
**设计意义**对高弹跳次数≥3 次)给予额外奖励,进一步激励长期稳定控制。使用 sigmoid 激活函数使奖励平滑增长。
**计算公式**
$$
\begin{aligned}
\sigma_{high} &= \frac{1}{1 + e^{-(n_{bounces} - 2.0) / 0.5}} &&\text{(高弹跳激活)} \\
r_{high} &= n_{bounces} \times 0.15 \times q_{bounce} \times \sigma_{high} &&\text{(额外奖励)}
\end{aligned}
$$
**权重**0.3
#### 9. 挡板-球水平对齐奖励
**设计意义**激励挡板主动移动到球的正下方而不是等待球落下。垂直距离越近权重越大在即将击打时刻要求更精确的对齐。弹跳时刻给予额外奖励boost强化正确的击打行为。
**计算公式**
$$
\begin{aligned}
\text{err}_{align} &= \|(\mathbf{x}_{ball}, \mathbf{y}_{ball}) - (\mathbf{x}_{paddle}, \mathbf{y}_{paddle})\|_2 &&\text{(对齐误差)} \\
w_{prox} &= e^{-d_{vert} / 0.1} &&\text{(垂直接近权重)} \\
q_{align} &= e^{-\frac{\text{err}_{align}^2}{2 \times 0.03^2}} \times (1.0 + 2.0 \times w_{prox}) &&\text{(对齐质量)} \\
k_{boost} &= \begin{cases} 3.0 & \text{if bounce detected} \\ 1.0 & \text{otherwise} \end{cases} &&\text{(弹跳增益)} \\
r_{align} &= q_{align} \times k_{boost} \times 0.3 &&\text{(对齐奖励)}
\end{aligned}
$$
**权重**0.6
#### 10. 挡板原位奖励
**设计意义**激励挡板在球远离时回到原位home position避免挡板长时间停留在高位。使用距离动态因子当球远离挡板时$d_{vert} > 0.15$ m增大奖励鼓励挡板快速回位当球接近时减小奖励允许挡板上移准备击打。这种设计使挡板运动更加节能和自然。
**计算公式**
$$
\begin{aligned}
\text{err}_{home} &= |z_{paddle} - z_{home}| &&\text{(原位偏离)} \\
k_{dist} &= 1.0 + \frac{0.5}{1 + e^{-(d_{vert} - 0.15) / 0.03}} &&\text{(距离因子)} \\
q_{home} &= e^{-\frac{\text{err}_{home}^2}{2 \times 0.05^2}} &&\text{(原位质量)} \\
r_{home} &= q_{home} \times k_{dist} &&\text{(原位奖励)} \\
\\
\text{其中:} \quad &z_{home} = 0.05 \text{ m} &&\text{(挡板原位高度)}
\end{aligned}
$$
**权重**1.5
### 惩罚项
#### 1. 过高向上速度惩罚
**设计意义**:防止球速度过快(>3.5 m/s失去控制确保球的运动在可控范围内。
**计算公式**
$$
r_{excess} = -\frac{1.0}{1 + e^{-(v_z - 3.5) / 0.3}} \qquad \text{sigmoid 惩罚)}
$$
**权重**1.0
#### 2. 向下速度惩罚
**设计意义**:惩罚球向下运动($v_z < -0.2$ m/s激励策略及时击打球避免球自由下落。
**计算公式**
$$
\begin{aligned}
\text{mag}_{down} &= -v_z \times \text{clip}(-v_z \times 0.3, 0, 0.5) &&\text{(向下幅度)} \\
\sigma_{down} &= \frac{1}{1 + e^{(v_z + 0.2) / 0.2}} &&\text{(向下激活)} \\
r_{down} &= \text{mag}_{down} \times \sigma_{down} &&\text{(向下惩罚)}
\end{aligned}
$$
**权重**1.0
#### 3. 挡板高度违规惩罚
**设计意义**:对挡板偏离原位过远(>0.1 m施加强惩罚确保挡板不会长时间停留在高位。
**计算公式**
$$
\begin{aligned}
\text{viol}_{height} &= \max(0, \text{err}_{home} - 0.1) &&\text{(违规量)} \\
r_{violation} &= -20.0 \times \text{viol}_{height} &&\text{(强惩罚)}
\end{aligned}
$$
**权重**1.0
#### 4. 动作变化率惩罚
**设计意义**:惩罚动作的剧烈变化,鼓励平滑的控制策略。
**计算公式**
$$
\begin{aligned}
r_{action} &= -\|\mathbf{a}_t - \mathbf{a}_{t-1}\|^2 &&\text{L2 惩罚)} \\
\\
\text{其中:} \quad &\mathbf{a}_t \in \mathbb{R}^6 &&\text{(当前动作向量)}
\end{aligned}
$$
**权重**$10^{-4}$
#### 5. 关节速度惩罚
**设计意义**:惩罚过高的关节速度,鼓励节能和平滑的运动。
**计算公式**
$$
\begin{aligned}
r_{joint\_vel} &= -\sum_{i=1}^{6} \dot{q}_i^2 &&\text{(速度平方和惩罚)} \\
\\
\text{其中:} \quad &\dot{\mathbf{q}} = [\dot{q}_1, \dot{q}_2, \dot{q}_3, \dot{q}_4, \dot{q}_5, \dot{q}_6]^T &&\text{(关节角速度向量)}
\end{aligned}
$$
**权重**$10^{-4}$
### 总奖励计算
$$
\begin{aligned}
R_{total} = &\ 2.0 \cdot r_{pos} \\
&+ 1.0 \cdot r_{out} \\
&+ 2.0 \cdot r_{vel} \\
&+ 4.5 \cdot r_h \\
&+ 1.0 \cdot r_{progress} \\
&+ 1.5 \cdot r_{controlled} \\
&+ 0.8 \cdot r_{bounce} \\
&+ 0.3 \cdot r_{high} \\
&+ 0.6 \cdot r_{align} \\
&+ 1.5 \cdot r_{home} \\
&+ 1.0 \cdot r_{excess} \\
&+ 1.0 \cdot r_{down} \\
&+ 1.0 \cdot r_{violation} \\
&+ 10^{-4} \cdot r_{action} \\
&+ 10^{-4} \cdot r_{joint\_vel}
\end{aligned}
$$
---
@@ -89,7 +366,43 @@ Bounce Ball 是一个单臂机器人操作任务,使用 6 自由度的配天 A
- **球掉落**:球的 z 坐标 < 0.05m(接近地面)
- **球过高**:球的 z 坐标 > 目标高度 + 1.0m(失去控制)
- **水平偏离过远**:球的 x 坐标绝对值 > 1.5m
- **水平偏离过远**:球的 x 或 y 坐标绝对值 > 1.5m
- **关节速度过高**:任一关节角速度 > 2π rad/s360°/s
- **超时**Episode 时长超过最大允许时间
---
## 初始状态
### 机器人初始化
**关节角度**
- 默认角度:[0°, 40°, 110°, 0°, -60°, 0°]
- 随机噪声:每个关节在 [-0.1, 0.1] 弧度范围内添加均匀随机噪声
**关节速度**
- 初始化为零,带有小幅随机噪声
### 球初始化
**位置**
- 基准位置:配置文件中的 `ball_init_pos`(默认 [0.58856, 0, 0.45] m
- 随机噪声:在 [-0.01, 0.01] m 范围内添加均匀随机噪声
**速度**
- 初始化为零
**姿态**
- 四元数:[0, 0, 0, 1](单位四元数,无旋转)
### 目标高度
每个环境的目标高度在 [0.4, 0.6] m 范围内随机采样,提升策略的泛化能力。
---
@@ -123,8 +436,14 @@ uv run scripts/play.py --env bounce_ball
## 预期训练结果
1. 连续弹跳:能够实现 3 次以上的连续弹跳
2. 位置控制:球的水平位置x 坐标)稳定在目标位置 ± 0.05m 范围内
3. 高度控制:球的高度稳定在目标高度 0.8 ± 0.1m 范围内
4. 速度控制球的向上速度保持在合理范围0.1-1.5 m/s
5. 稳定控制:能够持续 20 秒的稳定弹跳而不掉落
1. **连续弹跳**:能够实现 3 次以上的连续弹跳
2. **位置控制**:球的水平位置稳定在目标位置 ± 0.05m 范围内
3. **高度控制**:球的高度稳定在目标高度 ± 0.1m 范围内
4. **速度控制**球的向上速度保持在合理范围0.1-1.5 m/s
5. **稳定控制**:能够持续 20 秒的稳定颠球而不触发终止条件
---
## 已知问题
- **JAX 后端训练效果不佳**:当前 JAX 版本的训练效果不理想。建议使用 PyTorch 后端进行该环境的训练以获得更好的效果。

View File

@@ -0,0 +1,169 @@
# 机械手抓球
Bring Ball 是 DeepMind Control Suite 中的 Manipulator 经典任务平面机械手带拇指与手指需要抓取球体并将其移动到目标球位置。MotrixLab 当前提供 1 个 Bring Ball 环境:
- `dm-manipulator-bring-ball`:抓取球体并移动到目标位置
```{video} /_static/videos/bring-ball.mp4
:poster: _static/images/poster/bring-ball.jpg
:nocontrols:
:autoplay:
:playsinline:
:muted:
:loop:
:width: 100%
```
---
## 任务描述
Bring Ball 是一个二维平面x-z内的抓取与搬运任务
- 机械手包含 4 个手臂关节(`arm_root`、`arm_shoulder`、`arm_elbow`、`arm_wrist`)以及拇指/手指关节
- 夹持通过腱 `grasp` 同时驱动 `thumb` 与 `finger` 关节闭合
- 球体可在平面内滑动(`ball_x`、`ball_z`)并绕 y 轴旋转(`ball_y`
- 目标球为 mocap 物体,位置在 reset 时随机采样
---
## 动作空间Action Space
| 项目 | 详细信息 |
| -------- | ------------------------------- |
| **类型** | `Box(-1.0, 1.0, (5,), float32)` |
| **维度** | 5 |
动作对应如下(控制信号施加在关节/腱上):
| 序号 | 动作含义 | 最小值 | 最大值 | 对应 XML 中名称 |
| ---: | ----------------------------- | :----: | :----: | :-------------: |
| 0 | 根部关节驱动 | -1 | 1 | `root` |
| 1 | 肩部关节驱动 | -1 | 1 | `shoulder` |
| 2 | 肘部关节驱动 | -1 | 1 | `elbow` |
| 3 | 腕部关节驱动 | -1 | 1 | `wrist` |
| 4 | 夹持驱动(拇指/手指耦合闭合) | -1 | 1 | `grasp` |
---
## 观察空间
| 项目 | 详细信息 |
| -------- | -------------------------------- |
| **类型** | `Box(-inf, inf, (41,), float32)` |
| **维度** | 41 |
观察向量由以下部分组成(按顺序):
| 部分 | 内容说明 | 维度 | 备注 |
| -------------- | --------------------------- | ---- | ------------------------------------ |
| **arm_pos** | 8 个关节角度的 `sin`/`cos` | 16 | 关节顺序见下表,`sin`/`cos` 交替排列 |
| **arm_vel** | 8 个关节角速度 | 8 | 顺序同 arm_pos |
| **touch** | 触觉传感器 `log(1 + touch)` | 5 | palm/finger/thumb/fingertip/thumbtip |
| **hand_pos** | 抓取点 `grasp` 的世界坐标 | 3 | x, y, z |
| **object_pos** | 球体位置 | 3 | x, y, z |
| **target_pos** | 目标球位置 | 3 | x, y, z |
| **rel** | `object_pos - target_pos` | 3 | 相对位置 |
| 序号 | 观察量范围 | 维度 | 备注 |
| ----- | ---------------------------------------------- | ---- | ----------------------------- |
| 0-15 | 8 个关节角度的 `sin`/`cos` 交替排列 | 16 | 关节顺序arm_root → thumbtip |
| 16-23 | 8 个关节角速度 | 8 | 顺序同上 |
| 24-28 | 触觉palm, finger, thumb, fingertip, thumbtip | 5 | `log(1 + touch)` |
| 29-31 | grasp 位置 (x, y, z) | 3 | hand_pos |
| 32-34 | 球体位置 (x, y, z) | 3 | object_pos |
| 35-37 | 目标球位置 (x, y, z) | 3 | target_pos |
| 38-40 | 球体相对目标位置 (x, y, z) | 3 | rel |
关节顺序为:`arm_root`、`arm_shoulder`、`arm_elbow`、`arm_wrist`、`finger`、`fingertip`、`thumb`、`thumbtip`。
---
## 奖励函数设计
Bring Ball 使用 shaped 奖励,由多个子项加权组合,并加入惩罚项:
```python
# R1: Reach - 手指接近球
r_reach = tolerance(avg_tip_dist)
# R2: Orient - 手掌朝向球体
r_orient = clip(1 - orient_bound + dot(hand_dir, unit_vec_to_ball), 0..1)
# R3: Pause - 靠近球体时抑制臂部抖动
r_pause = tolerance(arm_speed_step) * is_close_to_ball
# R4: Close - 夹持动作与接触条件联合
r_close = r_close_intent * (approach_or_grasp)
# R5: Lift & Transport - 抬升高度 + 接近目标
r_lift_height = tolerance(ball_z)
r_transport = tolerance(move_dist_to_target)
r_lift = mix(r_lift_height, r_transport)
# Precision/Progress - 目标精度与移动进度
r_precision = tolerance(move_dist_to_target, gaussian)
r_progress = (prev_dist - curr_dist) * scale
# Penalty - 侧面接触与悬停惩罚
penalty_side + penalty_hover
```
默认权重(`BringBallCfg`
- reach 1.0、orient 1.5、pause 0.5、close 2.0、lift 6.0、precision 1.0
- lift 内部由 `lift_height_weight` 与 `transport_weight` 组合
- 进度奖励由 `transport_progress_scale` 控制
---
## 初始状态
- **手臂初始化**:默认使用模型初始姿态(`randomize_arm=False`),拇指/手指对称
- **目标位置**`x ∈ [-0.4, 0.4]``z ∈ [0.1, 0.4]``y = 0.001`
- **球体位置**`x ∈ [-0.4, 0.4]``z ∈ [0.2, 0.7]`,并与手部保持最小距离
- **物理稳定**reset 后会进行若干步 settle`settle_steps=300`
---
## Episode 终止条件
- 若观测中出现 `NaN` 值则终止
---
## 使用指南
### 1. 环境预览(随机动作)
```bash
uv run scripts/view.py --env dm-manipulator-bring-ball
```
### 2. 开始训练
建议显式指定训练后端JAX / PyTorch 二选一):
```bash
uv run scripts/train.py --env dm-manipulator-bring-ball --train-backend torch
```
### 3. 查看训练进度
```bash
uv run tensorboard --logdir runs/dm-manipulator-bring-ball
```
### 4. 测试训练结果
```bash
uv run scripts/play.py --env dm-manipulator-bring-ball
```
---
## 预期训练结果
1. 机械手能稳定接近并夹持球体
2. 球体被抬离地面并保持稳定高度
3. 球体最终能稳定到达目标球附近

View File

@@ -127,5 +127,6 @@ uv run scripts/play.py --env dm-cheetah
## 预期训练结果
1. 接近或超过 30.0 m/s 的稳定水平速度
2. 保持躯干直立且步态协调
1. 接近或超过 10.0 m/s 的稳定水平速度奔跑
2. 保持躯体稳定性且步态协调,长距离奔跑不摔倒
3. 奔跑姿态接近真实猎豹,奔跑过程具有伸展感

View File

@@ -0,0 +1,193 @@
# Finger 机械手指
Finger 是 DeepMind Control Suite 中的经典操控任务一个由两段连杆组成的“手指”通过施加关节力矩与旋转拨片spinner交互。MotrixLab 当前提供了 3 个 Finger 相关环境:
```{video} /_static/videos/dm_finger_spin.mp4
:poster: _static/images/poster/dm_finger_spin.jpg
:nocontrols:
:autoplay:
:playsinline:
:muted:
:loop:
:width: 100%
```
```{video} /_static/videos/dm_finger_turn.mp4
:poster: _static/images/poster/dm_finger_turn.jpg
:nocontrols:
:autoplay:
:playsinline:
:muted:
:loop:
:width: 100%
```
- `dm-finger-spin`:让 spinner 持续向指定方向旋转
- `dm-finger-turn-easy`:将 spinner 的顶端tip对准目标点较大目标半径
- `dm-finger-turn-hard`:同 Turn但目标半径更小
---
## 任务描述
Finger 是一个二维平面x-z内的手指与旋转拨片交互任务
- 手指有 2 个受控关节:`proximal`、`distal`
- spinner 通过关节 `hinge` 转动,`tip` 表示 spinner 顶端位置
- Turn 任务会在 spinner 周围随机采样一个目标点target
---
## 动作空间Action Space
| 项目 | 详细信息 |
| -------- | ------------------------------- |
| **类型** | `Box(-1.0, 1.0, (2,), float32)` |
| **维度** | 2 |
动作对应如下:
| 序号 | 动作含义(施加在关节的力矩) | 最小值 | 最大值 | 对应 XML 中名称 |
| ---: | ---------------------------- | :----: | :----: | :-------------: |
| 0 | 近端关节 `proximal` 驱动扭矩 | -1 | 1 | `proximal` |
| 1 | 远端关节 `distal` 驱动扭矩 | -1 | 1 | `distal` |
---
## 观察空间
Finger 环境的观测以 dm_control 的 observation dict 为参考,但在 MotrixLab 中被拼成一个向量。
### Spin 观察空间
| 项目 | 详细信息 |
| -------- | ------------------------------- |
| **类型** | `Box(-inf, inf, (9,), float32)` |
| **维度** | 9 |
组成如下(按顺序):
| 部分 | 内容说明 | 维度 | 备注 |
| ------------ | --------------------------------------------- | ---- | ----------------------------- |
| **position** | `qpos(proximal, distal)` + `tip_xz` | 4 | tip 相对 spinner 的 x、z 位置 |
| **velocity** | `qvel(proximal, distal, hinge)` | 3 | hinge 角速度用于 Spin 奖励 |
| **touch** | `log(1 + touch_top)`、`log(1 + touch_bottom)` | 2 | 触觉传感器的对数压缩 |
| 序号 | 观察量 | 最小值 | 最大值 | XML/Sensor 名称 | 类型 (单位) |
| ---- | ---------------------------------- | ------ | ------ | ----------------------- | -------------- |
| 0 | `proximal` 关节角度 | -Inf | Inf | `proximal` | 角度 (rad) |
| 1 | `distal` 关节角度 | -Inf | Inf | `distal` | 角度 (rad) |
| 2 | tip 相对 spinner 的 x 位移 | -Inf | Inf | `framepos(tip/spinner)` | 位置 (m) |
| 3 | tip 相对 spinner 的 z 位移 | -Inf | Inf | `framepos(tip/spinner)` | 位置 (m) |
| 4 | `proximal` 角速度 | -Inf | Inf | `proximal_velocity` | 角速度 (rad/s) |
| 5 | `distal` 角速度 | -Inf | Inf | `distal_velocity` | 角速度 (rad/s) |
| 6 | spinner 的 `hinge` 角速度 | -Inf | Inf | `hinge_velocity` | 角速度 (rad/s) |
| 7 | 触觉(上侧)`log(1 + touchtop)` | -Inf | Inf | `touchtop` | 无量纲 |
| 8 | 触觉(下侧)`log(1 + touchbottom)` | -Inf | Inf | `touchbottom` | 无量纲 |
### Turn 观察空间
| 项目 | 详细信息 |
| -------- | -------------------------------- |
| **类型** | `Box(-inf, inf, (12,), float32)` |
| **维度** | 12 |
相较 Spin 额外增加:
| 部分 | 内容说明 | 维度 | 备注 |
| ------------------- | -------------------------------- | ---- | ------------------------- |
| **target_position** | target 相对 spinner 的 x、z 坐标 | 2 | target 在 reset 时采样 |
| **dist_to_target** | tip 到 target 球面距离(带符号) | 1 | 负值表示 tip 落在目标球内 |
向量最后 3 个维度为:`target_x`, `target_z`, `dist_to_target`。
---
## 奖励函数设计
### Spin
在 dm_control 中Spin 的稀疏奖励通常由 spinner 的角速度阈值触发。MotrixLab 默认使用更易训练的 dense/shaped 奖励,并同时在 info 中记录稀疏版本:
```python
# hinge_velocity 为 spinner 关节角速度
spin_sparse = 1 if hinge_velocity <= -15 else 0
# shaped: clip(-hinge_velocity / 15, 0..1)
spin = clip(-hinge_velocity / 15, 0, 1)
```
### TurnEasy / Hard
Turn 的核心是 tip 触达并对准目标点:目标点位于 spinner 周围一圈,目标球半径在 easy/hard 中不同。
- `turn_sparse = 1` 当 tip 进入目标球内部(`dist_to_target <= 0`
- 默认 shaped 奖励以 `dist_to_target` 的指数衰减为主,并额外加入“靠近 spinner、增加接触、抑制动作抖动”等项最终裁剪到 `[0, 1]`
---
## 初始状态
- `proximal`、`distal` 关节角:在各自关节限制范围内均匀采样
- spinner 的 `hinge` 角:`[-pi, pi]` 均匀采样
- Turn 任务:每个 episode 在 spinner 周围采样 target角度均匀采样位置落在 x-z 平面)
## Episode 终止条件
- 若观测中出现 `NaN` 值则终止
---
## 使用指南
### 1. 环境预览(随机动作)
```bash
uv run scripts/view.py --env dm-finger-spin
```
```bash
uv run scripts/view.py --env dm-finger-turn-easy
```
```bash
uv run scripts/view.py --env dm-finger-turn-hard
```
### 2. 开始训练
建议显式指定训练后端JAX / PyTorch 二选一):
```bash
uv run scripts/train.py --env dm-finger-spin --train-backend torch
```
```bash
uv run scripts/train.py --env dm-finger-turn-easy --train-backend torch
```
```bash
uv run scripts/train.py --env dm-finger-turn-hard --train-backend torch
```
### 3. 查看训练进度
```bash
uv run tensorboard --logdir runs/dm-finger-spin
```
### 4. 测试训练结果
`scripts/play.py` 默认会自动在 `runs/{env-name}/` 下寻找最新的 `best_agent.*`,也可以用 `--policy` 显式指定:
```bash
uv run scripts/play.py --env dm-finger-turn-hard
```
---
## 预期训练结果
1. `dm-finger-spin`spinner 能稳定持续向目标方向旋转hinge_velocity 达到阈值附近)
2. `dm-finger-turn-easy`:手指能稳定接触并将 tip 对准目标点(成功率较高、抖动较小)
3. `dm-finger-turn-hard`:能对准更小目标,但更容易出现“接触不足/动作抖动”的训练难点

View File

@@ -0,0 +1,236 @@
# 三维人形机器人
三维人形机器人Humanoid是 DeepMind Control Suite 中的经典双足行走任务。其目标是训练一个模拟的三维人形机器人,通过控制其关节力矩,实现站立、行走和奔跑。
```{video} /_static/videos/dm_humanoid_run.mp4
:poster: _static/images/poster/dm_humanoid_run.jpg
:nocontrols:
:autoplay:
:playsinline:
:muted:
:loop:
:width: 100%
```
## 任务描述
Humanoid 是一个三维空间的双足人形机器人任务。机器人由头部、躯干、双臂和双腿组成,拥有 21 个受控关节(执行器),智能体通过向这些关节施加扭矩作为动作,让机器人实现站立平衡、向前行走或快速奔跑。该任务要求协调的双足步态、平衡控制和三维空间姿态稳定能力。
---
## 动作空间Action Space
| 项目 | 详细信息 |
| -------- | -------------------------------- |
| **类型** | `Box(-1.0, 1.0, (21,), float32)` |
| **维度** | 21 |
动作对应如下:
| 序号 | 动作含义(施加在关节的力矩) | 最小值 | 最大值 | 对应 XML 中名称 |
| ---: | ---------------------------- | :----: | :----: | :---------------- |
| 0 | 腹部 Y 轴旋转关节驱动扭矩 | -1.0 | 1.0 | `abdomen_y` |
| 1 | 腹部 Z 轴旋转关节驱动扭矩 | -1.0 | 1.0 | `abdomen_z` |
| 2 | 腹部 X 轴旋转关节驱动扭矩 | -1.0 | 1.0 | `abdomen_x` |
| 3 | 右髋关节 X 轴驱动扭矩 | -1.0 | 1.0 | `right_hip_x` |
| 4 | 右髋关节 Z 轴驱动扭矩 | -1.0 | 1.0 | `right_hip_z` |
| 5 | 右髋关节 Y 轴驱动扭矩 | -1.0 | 1.0 | `right_hip_y` |
| 6 | 右膝关节驱动扭矩 | -1.0 | 1.0 | `right_knee` |
| 7 | 右踝关节 X 轴驱动扭矩 | -1.0 | 1.0 | `right_ankle_x` |
| 8 | 右踝关节 Y 轴驱动扭矩 | -1.0 | 1.0 | `right_ankle_y` |
| 9 | 左髋关节 X 轴驱动扭矩 | -1.0 | 1.0 | `left_hip_x` |
| 10 | 左髋关节 Z 轴驱动扭矩 | -1.0 | 1.0 | `left_hip_z` |
| 11 | 左髋关节 Y 轴驱动扭矩 | -1.0 | 1.0 | `left_hip_y` |
| 12 | 左膝关节驱动扭矩 | -1.0 | 1.0 | `left_knee` |
| 13 | 左踝关节 X 轴驱动扭矩 | -1.0 | 1.0 | `left_ankle_x` |
| 14 | 左踝关节 Y 轴驱动扭矩 | -1.0 | 1.0 | `left_ankle_y` |
| 15 | 右肩关节 1 驱动扭矩 | -1.0 | 1.0 | `right_shoulder1` |
| 16 | 右肩关节 2 驱动扭矩 | -1.0 | 1.0 | `right_shoulder2` |
| 17 | 右肘关节驱动扭矩 | -1.0 | 1.0 | `right_elbow` |
| 18 | 左肩关节 1 驱动扭矩 | -1.0 | 1.0 | `left_shoulder1` |
| 19 | 左肩关节 2 驱动扭矩 | -1.0 | 1.0 | `left_shoulder2` |
| 20 | 左肘关节驱动扭矩 | -1.0 | 1.0 | `left_elbow` |
---
## 观察空间
| 项目 | 详细信息 |
| -------- | -------------------------------- |
| **类型** | `Box(-inf, inf, (73,), float32)` |
| **维度** | 73 |
Humanoid 环境的观测空间由以下部分组成(按顺序):
| 部分 | 内容说明 | 维度 | 备注 |
| ------------------ | ------------------------------------- | ---- | ------------------------------------------------- |
| **joint_angles** | 各关节角度(排除根关节的 7 个自由度) | 22 | 22 个关节的角度信息 |
| **head_height** | 头部高度 | 1 | 头部相对于地面的高度 |
| **extremities** | 四肢末端位置(相对于躯干) | 12 | 左手、左脚、右手、右脚的位置(各 3 维,按此顺序) |
| **torso_vertical** | 躯干垂直方向向量 | 3 | 躯干在局部坐标系中的垂直方向 |
| **com_vel** | 质心线速度 | 3 | 躯干子树的线速度 |
| **qvel** | 所有关节和根部的速度信息 | 29 | 包括根关节的 6 个自由度 |
| **target_local** | 目标方向(局部坐标系) | 3 | 目标方向在躯干局部坐标系中的表示 |
---
## 奖励函数设计
Humanoid 的奖励函数根据任务类型(站立、行走、奔跑)有所不同,但都包含以下核心组件:
### 姿态奖励Posture Reward
```python
# 头部高度奖励:保持头部在目标高度(标准站立高度的 95%,约 1.33m)以上
stand_reward = tolerance(head_height, bounds=(stand_height * 0.95, inf), margin=0.5)
# 躯干直立奖励:保持躯干直立
upright_reward = tolerance(torso_upright, bounds=(0.9, inf), sigmoid="linear", margin=0.9)
# 盆骨高度奖励:保持盆骨在合理高度(标准站立高度的 60%,约 0.84m)以上
pelvis_height_reward = tolerance(pelvis_height, bounds=(stand_height * 0.6, inf), sigmoid="linear", margin=stand_height * 0.6)
# 姿态奖励 = 头部高度奖励 × 躯干直立奖励 × 盆骨高度奖励
posture_reward = stand_reward * upright_reward * pelvis_height_reward
```
### 速度奖励Speed Reward
根据任务类型,速度奖励的计算方式不同:
**站立任务move_speed <= 0**
```python
# 速度奖励:保持接近零速度
speed_reward = tolerance(actual_speed, bounds=(0, 0), margin=1.0, value_at_margin=0.01)
```
**行走任务0 < move_speed <= 3.0**
```python
# 速度奖励在目标方向X 轴正方向)上达到目标速度(默认 1.0 m/s
actual_speed = dot(com_vel[:2], target_direction[:2]) # 速度在目标方向上的投影
speed_reward = tolerance(actual_speed, bounds=(move_speed, move_speed), margin=move_speed, sigmoid="linear")
```
**奔跑任务move_speed > 3.0**
```python
# 速度奖励:在目标方向上达到目标速度(默认 10.0 m/s以上
actual_speed = dot(com_vel[:2], target_direction[:2])
speed_reward = tolerance(actual_speed, bounds=(move_speed, inf), margin=move_speed, sigmoid="linear")
```
### 能量奖励Energy Reward
```python
energy_reward = exp(-energy_coef * mean(ctrls ^ 2))
```
### 步态奖励Gait Reward
```python
# 躯干朝向奖励:躯干正对目标方向
torso_heading_reward = tolerance(dot(torso_forward, target_dir), bounds=(0.9, 1.0), margin=0.3, sigmoid="linear")
# 头部朝向奖励:头部正对目标方向
head_heading_reward = tolerance(dot(head_forward, target_dir), bounds=(0.9, 1.0), margin=0.3, sigmoid="linear")
# 盆骨朝向奖励:盆骨正对目标方向
pelvis_yaw_reward = tolerance(dot(pelvis_forward, target_dir), bounds=(0.9, 1.0), margin=0.3, sigmoid="linear")
# 盆骨水平奖励:盆骨保持水平
pelvis_level_reward = tolerance(pelvis_up, bounds=(0.9, 1.0), margin=0.3, sigmoid="linear")
# 足部高度奖励:足部保持贴近地面
feet_height_reward = tolerance(max_foot_height, bounds=(0.0, 0.3), margin=0.5, sigmoid="quadratic")
# 步态奖励 = 所有朝向和姿态奖励的乘积
gait_reward = torso_heading_reward * head_heading_reward * pelvis_yaw_reward * pelvis_level_reward * feet_height_reward
```
### 总奖励
```python
total_reward = posture_reward * speed_reward * energy_reward * gait_reward
```
---
## 初始状态
- **机器人位置**:躯干初始高度为 1.33 米(标准站立高度的 95%
- **机器人姿态**:躯干保持直立,四元数设置为 (1.0, 0.0, 0.0, 0.0)
- **关节角度**:在关节限位范围内随机初始化
- 躯干/髋部基础关节在较小范围内随机化±15 度)
- 腿部关节:对称初始化,确保左右腿独立随机化,膝盖初始弯曲
- 手臂关节:对称初始化,使用关节限位的中间 80% 范围
- **初始速度**:所有关节速度和线速度初始化为接近零的小随机值(-0.01 到 0.01
- **初始控制**:所有执行器控制量初始化为接近零的小随机值(-0.02 到 0.02
## Episode 终止条件
- 机器人状态观测值出现异常数值NaN 或 Inf
- 头部高度过低:头部高度低于标准站立高度的 50%0.7 米)
- 躯干倾斜过大:躯干垂直分量小于 0.2(躯干严重倾斜)
- 速度异常:任何关节速度的绝对值超过 200.0 rad/s 或 m/s
- Episode 最大时长25 秒
---
## 使用指南
### 1. 环境预览
```bash
uv run scripts/view.py --env dm-humanoid-stand
uv run scripts/view.py --env dm-humanoid-walk
uv run scripts/view.py --env dm-humanoid-run
```
### 2. 开始训练
```bash
uv run scripts/train.py --env dm-humanoid-stand
uv run scripts/train.py --env dm-humanoid-walk
uv run scripts/train.py --env dm-humanoid-run
```
### 3. 查看训练进度
```bash
uv run tensorboard --logdir runs/dm-humanoid-walk
```
### 4. 测试训练结果
```bash
uv run scripts/play.py --env dm-humanoid-stand
uv run scripts/play.py --env dm-humanoid-walk
uv run scripts/play.py --env dm-humanoid-run
```
---
## 预期训练结果
### 站立任务 (dm-humanoid-stand)
1. 头部高度保持在 1.3-1.5m 范围
2. 躯干直立角度偏差小于 15 度
3. 能够稳定站立不倒
4. 速度接近零,无明显移动
### 行走任务 (dm-humanoid-walk)
1. 实际行走速度接近 1.0 m/s
2. 步态协调,无明显摔倒
3. 能够持续稳定行走
4. 躯干和头部朝向目标方向
### 奔跑任务 (dm-humanoid-run)
1. 奔跑速度达到 4.0-10.0 m/s
2. 出现飞行相(双脚同时离地)
3. 步态协调稳定
4. 能够保持高速奔跑姿态

View File

@@ -0,0 +1,138 @@
# 单摆训练示例
单摆Pendulum是经典的单关节摆起并倒立保持任务目标是用一个电机扭矩把摆甩起并稳定在倒立位置。
```{video} /_static/videos/pendulum.mp4
:poster: _static/images/poster/pendulum.jpg
:nocontrols:
:autoplay:
:playsinline:
:muted:
:loop:
:width: 100%
```
---
## 任务描述
单摆由一段杆体和一个铰接关节组成关节由单个电机驱动gear 可配置)。电机施加的扭矩使摆杆在平面内旋转,实现从任意初始角度的摆起、倒立和保持。关节力矩受限于 ctrlrange通过控制扭矩大小与方向策略需要完成能量累积swing-up并在倒立位置维持平衡同时抑制角速度带来的震荡。
---
## 动作空间
| 项目 | 详情 |
| -------- | ------------------------------- |
| **类型** | `Box(-1.0, 1.0, (1,), float32)` |
| **维度** | 1 |
---
## 观察空间
| 项目 | 详情 |
| -------- | ------------------------------- |
| **类型** | `Box(-inf, inf, (3,), float32)` |
| **维度** | 3 |
顺序:`cos(theta), sin(theta), 角速度`。
---
## 奖励设计
- 倒立奖励:鼓励角度围绕 π(倒立)
- 能量 shaping能量接近倒立位置
- 惩罚:`角速度^2`、`ctrl^2`、`(ctrl - prev_ctrl)^2`,抑制震荡与过猛动作
---
## 初始状态
- 角度随机于 `[-pi, pi]`
- 角速度小噪声(如配置)
- 控制历史 `prev_ctrl` 初始化为 0
## Episode 终止条件
- 无跌倒终止;仅 NaN 检查
- Episode 长度由 `max_episode_seconds` 限制
---
### 1. 环境预览
```bash
uv run scripts/view.py --env pendulum
```
### 2. 开始训练
```bash
# 默认参数训练
uv run scripts/train.py --env pendulum
# 自定义并行环境数
uv run scripts/train.py --env pendulum --num-envs 1024
# 开启训练时渲染
uv run scripts/train.py --env pendulum --render
```
### 3. 查看训练进度
```bash
uv run tensorboard --logdir runs/pendulum
```
### 4. 测试训练结果
```bash
# 自动寻找最新/最优策略(推荐)
uv run scripts/play.py --env pendulum
# 手动指定策略文件
uv run scripts/play.py --env pendulum --policy runs/pendulum/nn/best_policy.pickle
```
> **提示**:策略默认在 `runs/pendulum/` 下自动发现,可用 `--policy` 手动指定。
---
## 配置参数
### 环境配置(示例)
```python
@dataclass
class PendulumEnvCfg(EnvCfg):
model_file: str = ".../pendulum.xml" # MJCF 模型,默认 gear=5
max_episode_seconds: float = 20.0
sim_dt: float = 0.0125
ctrl_dt: float = 0.025
```
### 训练配置(示例 PPO
```python
@rlcfg("pendulum")
@dataclass
class PendulumPPO(PPOCfg):
seed: int = 42
max_env_steps: int = 10_000_000
num_envs: int = 1024
learning_rate: float = 3e-4
rollouts: int = 32
learning_epochs: int = 5
mini_batches: int = 4
policy_hidden_layer_sizes: tuple[int, ...] = (64, 64)
value_hidden_layer_sizes: tuple[int, ...] = (64, 64)
```
---
## 预期训练结果
1. 摆能主动摆起并停留在倒立附近
2. 倒立处的震荡由角速度与控制变化惩罚抑制

View File

@@ -0,0 +1,370 @@
# Shadow Hand 立方体重定向
## 概述
本文档详细描述了基于 Shadow Hand 灵巧手的立方体重定向任务环境。该任务是机器人操作领域的经典基准测试,要求机器人在手中重新定向立方体以匹配随机目标姿态。
```{video} /_static/videos/shadow_hand_repose.mp4
:poster: _static/images/poster/shadow_hand_repose.jpg
:nocontrols:
:autoplay:
:playsinline:
:muted:
:loop:
:width: 100%
```
---
## 环境描述
Shadow Hand 立方体重定向任务基于真实的 Shadow Hand 24 自由度灵巧手构建,旨在训练机器人通过精细的多指协调,将手中的立方体旋转到目标姿态。
### 机器人结构
Shadow Hand 是一个高仿真的 24 自由度灵巧手,由以下主要部分组成:
- **手掌Palm**:手的基础结构,固定在基座上
- **5 个手指**
- **拇指Thumb**5 自由度,包括 CMC 旋转、MCP、IP 关节
- **食指Index**4 自由度,包括 MCP、PIP、DIP 关节
- **中指Middle**4 自由度,包括 MCP、PIP、DIP 关节
- **无名指Ring**4 自由度,包括 MCP、PIP、DIP 关节
- **小指Little**4 自由度,包括 MCP、PIP、DIP 关节
- **执行器配置**20 个驱动关节4 个耦合关节由其他关节驱动)
### 手指关节详情
每个手指(除拇指外)包含:
- **MCP 关节**掌指关节2 自由度(外展/内收 + 屈曲)
- **PIP 关节**近端指间关节1 自由度(屈曲)
- **DIP 关节**远端指间关节1 自由度(屈曲)
拇指包含:
- **CMC 关节**腕掌关节2 自由度
- **MCP 关节**掌指关节1 自由度
- **IP 关节**指间关节1 自由度
### 环境物体
- **立方体**50mm × 50mm × 50mm 的正方体
- 质量:约 0.028 kg
- 摩擦系数1.2
- 初始位置:手掌中心上方 `(0.33, 0.00, 0.295)` 米
- **目标可视化**半透明目标姿态指示器mocap body
### 任务目标
机器人需要完成以下操作目标:
1. **保持抓握**:维持立方体在手中的稳定抓握
2. **感知目标**:观察目标姿态(可视化指示器)
3. **精细操作**:通过多指协调旋转立方体
4. **姿态对齐**:将立方体姿态旋转至目标姿态(容差 ±0.1 弧度)
---
## 动作空间
动作空间为 `Box(-1, 1, (20,), float32)`,表示施加到 20 个驱动关节的位置控制指令(归一化)。
### 动作处理流程
```python
# 1. 缩放到执行器限制
targets = scale(actions, lower_limits, upper_limits)
# 2. 动作平滑(可选)
if act_moving_average < 1.0:
targets = α * targets + (1-α) * prev_actions
# 3. 裁剪到限制
targets = clip(targets, lower_limits, upper_limits)
# 4. 应用控制
actuator_ctrls = targets
```
### 动作维度详细说明
| 编号 | 手指 | 关节 | 自由度 | 说明 |
| ----- | ------ | ----- | ------ | ------------- |
| 0-4 | 拇指 | J0-J4 | 5 | CMC, MCP, IP |
| 5-8 | 食指 | J0-J3 | 4 | MCP, PIP, DIP |
| 9-12 | 中指 | J0-J3 | 4 | MCP, PIP, DIP |
| 13-16 | 无名指 | J0-J3 | 4 | MCP, PIP, DIP |
| 17-20 | 小指 | J0-J3 | 4 | MCP, PIP, DIP |
---
## 观测空间
观测空间为 `Box(-inf, inf, (157,), float32)`,包含机器人的本体感知信息、立方体状态、目标状态和指尖状态。
### 观测组成部分
观测向量由以下部分组成(按顺序):
#### 1. 手部关节状态48 维)
- **关节位置24 维)**:未缩放的原始关节角度
- **关节速度24 维)**:缩放 0.2 倍的关节角速度
#### 2. 立方体状态17 维)
- **位置3 维)**:世界坐标系中的立方体位置 `(x, y, z)`
- **姿态4 维)**:四元数 `(x, y, z, w)`
- **线速度3 维)**:立方体线速度
- **角速度3 维)**:缩放 0.2 倍的角速度
- **归一化因子**:速度观测乘以 `vel_obs_scale = 0.2`
#### 3. 目标状态11 维)
- **目标位置3 维)**:固定在 `(0.33, 0.00, 0.295)`
- **目标姿态4 维)**:随机采样的目标四元数
- **相对旋转4 维)**:立方体到目标的相对四元数
#### 4. 指尖状态65 维)
5 个指尖的状态,每个指尖 13 维:
- **位置3 维)**:指尖在笛卡尔空间的位置
- **姿态4 维)**:指尖四元数
- **速度6 维)**:线速度和角速度
**指尖链接名称**
- `rh_ffdistal`:食指指尖
- `rh_mfdistal`:中指指尖
- `rh_rfdistal`:无名指指尖
- `rh_lfdistal`:小指指尖
- `rh_thdistal`:拇指指尖
#### 5. 动作历史20 维)
- 上一步的动作值,用于策略的时间上下文
### 观测详细说明
| 编号 | 观测内容 | 维度 | 范围 | 单位 |
| ------- | ---------------------------- | ---- | ------------ | ------ |
| 0-23 | 手部关节位置(未归一化) | 24 | 关节限制范围 | rad |
| 24-47 | 手部关节速度(× 0.2 | 24 | ±π/2 | rad/s |
| 48-50 | 立方体位置 | 3 | 实数 | m |
| 51-54 | 立方体姿态(四元数 w,x,y,z | 4 | 单位范数 | 无量纲 |
| 55-57 | 立方体线速度 | 3 | 实数 | m/s |
| 58-60 | 立方体角速度(× 0.2 | 3 | 实数 | rad/s |
| 61-63 | 目标位置 | 3 | 固定值 | m |
| 64-67 | 目标姿态(四元数 w,x,y,z | 4 | 单位范数 | 无量纲 |
| 68-71 | 相对旋转(四元数 w,x,y,z | 4 | 单位范数 | 无量纲 |
| 72-136 | 指尖状态5×13 | 65 | - | - |
| 137-156 | 上一步动作 | 20 | [-1, 1] | 无量纲 |
---
## 奖励函数
奖励函数采用复合设计,包含多个奖励和惩罚项。
### 主要奖励项
1. **旋转对齐奖励**(核心目标)
```
rot_reward = rot_reward_scale / (|rot_dist| + rot_eps)
```
- **缩放系数**`1.0`
- **Epsilon**`0.1`
- **旋转距离计算**:使用四元数旋转距离公式
- **激励**:随着立方体姿态接近目标,奖励呈反比增长
2. **位置距离惩罚**
```
dist_reward = dist_reward_scale × goal_dist
```
- **缩放系数**`-10.0`
- **距离计算**:立方体到目标位置的欧氏距离
- **激励**:阻止立方体掉落,保持在目标位置附近
3. **动作正则化惩罚**
```
action_penalty = action_penalty_scale × ||actions||²
```
- **缩放系数**`-0.0002`
- **目的**:鼓励平滑、节能的运动
### 条件奖励
4. **成功奖励**
```
if |rot_dist| ≤ success_tolerance:
reward += reach_goal_bonus
```
- **奖励值**`2.0`
- **容差**`0.1` 弧度(约 5.7°)
- **目的**:达成目标对齐的稀疏奖励
5. **掉落惩罚**
```
if goal_dist ≥ fall_dist:
reward += fall_penalty
terminated = True
```
- **惩罚值**`0.0`(仅终止,无额外惩罚)
- **距离阈值**`0.24`
- **目的**:立方体掉落时终止回合
---
## 初始状态
### 手部初始化
**位置初始化:**
手掌固定在世界坐标系中,位置由模型文件确定。
**关节角度初始化:**
- 使用模型默认关节位置
- 添加均匀随机噪声:`[-0.2, 0.2]` 弧度
- 作用范围:所有 24 个手部自由度
**速度初始化:**
所有关节速度初始化为零。
### 立方体初始化
**位置初始化:**
- 固定位置:`(0.33, 0.00, 0.295)` 手掌中心上方)
- 添加均匀随机噪声:`[-0.01, 0.01]` ±1cm
**姿态初始化:**
- 使用 Shoemake 方法生成均匀分布的随机四元数
- 确保在 SO(3) 空间上的均匀采样
**速度初始化:**
所有线速度和角速度初始化为零。
### 目标初始化
**位置初始化:**
- 固定位置:`(0.33, 0.00, 0.295)`(与立方体初始位置相同)
**姿态初始化:**
- 使用 Shoemake 方法生成均匀分布的随机目标四元数
- 每次重置时重新采样
---
## 终止条件
回合在以下情况下终止:
1. **掉落终止**:立方体距离目标位置 ≥ `fall_dist`0.24m
2. **超时终止**:达到 `max_episode_steps`(默认 1000 步)
3. **NaN 保护**:检测到旋转距离或位置距离为 NaN
### 成功保持机制
环境使用连续成功计数器:
- 当满足旋转容差时,计数器递增
- 当达到 `max_consecutive_successes`50触发成功终止并重置目标
- 旋转容差:`0.1` 弧度
---
## 使用方法
### 训练
```bash
uv run scripts/train.py --env shadow-hand-repose
```
### 策略评估
```bash
uv run scripts/play.py --env shadow-hand-repose
```
### 环境可视化
```bash
uv run scripts/view.py --env shadow-hand-repose
```
### TensorBoard
```bash
uv run tensorboard --logdir runs/shadow-hand-repose
```
---
## 配置参数
### 环境参数
| 参数名 | 默认值 | 说明 |
| --------------------- | ------ | ------------------ |
| `max_episode_seconds` | 10.0 | 最大回合时长(秒) |
| `ctrl_dt` | 0.01 | 控制时间步(秒) |
| `max_episode_steps` | 1000 | 最大回合步数 |
| `num_hand_dofs` | 24 | 手部总自由度 |
| `num_actuators` | 20 | 驱动关节数量 |
### 奖励参数
| 参数名 | 默认值 | 说明 |
| ---------------------- | ------- | ---------------- |
| `dist_reward_scale` | -10.0 | 位置距离奖励 |
| `rot_reward_scale` | 1.0 | 旋转对齐奖励 |
| `rot_eps` | 0.1 | 旋转奖励 epsilon |
| `action_penalty_scale` | -0.0002 | 动作正则化 |
| `success_tolerance` | 0.1 | 成功容差(弧度) |
| `reach_goal_bonus` | 2.0 | 成功奖励 |
| `fall_dist` | 0.24 | 掉落距离阈值 |
| `fall_penalty` | 0.0 | 掉落惩罚 |
### 重置噪声参数
| 参数名 | 默认值 | 说明 |
| ---------------------- | ------ | ----------------------- |
| `reset_position_noise` | 0.01 | 立方体位置噪声(米) |
| `reset_dof_pos_noise` | 0.2 | 关节位置噪声(弧度) |
| `reset_dof_vel_noise` | 0.0 | 关节速度噪声(弧度/秒) |
### 观测缩放参数
| 参数名 | 默认值 | 说明 |
| --------------- | ------ | ---------------- |
| `vel_obs_scale` | 0.2 | 速度观测缩放因子 |
---
## 参考资料
该环境基于以下经典工作实现:
- **OpenAI Dactyl** (2018):首个成功的手内操作 sim-to-real 迁移
- **Isaac Gym** (2021):高性能 GPU 加速的物理仿真
- **Isaac Lab** (2023):模块化机器人学习框架

View File

@@ -0,0 +1,189 @@
# 容器部署
本文档介绍如何使用 Docker 容器化部署 MotrixLab以简化环境配置并实现快速部署。
## 前置要求
- **Docker****Docker Compose**: [安装文档](https://docs.docker.com/engine/install/)
- **NVIDIA Container Toolkit**: [安装文档](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html#installation)
- **NVIDIA GPU**: 支持 CUDA 12.8 的显卡
## 快速开始
### 1. 克隆项目仓库
```bash
git clone https://github.com/Motphys/MotrixLab.git
cd MotrixLab/docker
```
### 2. 使用 Docker Compose 启动
我们提供了完整的 `docker-compose.yml` 配置文件,支持一键启动训练和 TensorBoard 可视化服务。
```bash
# 启动训练和 TensorBoard 服务
docker compose up -d
```
这将启动以下服务:
- **motrixlab-training**: 训练容器,执行强化学习训练任务
- **motrixlab-tensorboard**: TensorBoard 可视化服务,通过浏览器访问 http://localhost:6006
### 3. 配置训练参数
您可以通过环境变量自定义训练配置:
```bash
# 设置训练后端jax 或 torch
export MOTRIX_TRAIN_BACKEND=jax
# 设置并行环境数量
export MOTRIX_NUM_ENVS=2048
# 设置训练环境名称
export MOTRIX_ENV=cartpole
# 启动服务
docker compose up -d
```
| 环境变量 | 默认值 | 说明 |
| :--------------------- | :--------- | :------------------------- |
| `MOTRIX_TRAIN_BACKEND` | `jax` | 训练后端:`jax``torch` |
| `MOTRIX_NUM_ENVS` | `2048` | 并行环境数量 |
| `MOTRIX_ENV` | `cartpole` | 训练环境名称 |
### 4. 查看训练进度
训练日志会自动保存到 Docker Volume `motrixlab-data` 中。您可以通过以下方式查看:
```bash
# 查看训练容器日志
docker logs -f motrixlab-training
# 访问 TensorBoard
# 浏览器打开: http://localhost:6006
```
### 5. 停止服务
```bash
# 停止所有服务
docker compose down
# 停止服务并删除数据卷
docker compose down -v
```
## 高级用法
### 构建 Docker 镜像
如果您需要自定义镜像,可以从源代码构建:
```bash
# 在项目根目录执行
cd docker
docker build -t motphys/motrixlab:latest .
```
### 运行单个容器
如果您只想运行训练容器而不使用 Docker Compose
```bash
docker run --gpus all \
-v $(pwd)/runs:/root/motrixlab/runs \
motphys/motrixlab:latest \
scripts/train.py --train-backend jax --num-envs 2048 --env cartpole
```
### 持久化训练结果
默认配置使用 Docker Volume `motrixlab-data` 保存训练结果。您可以将其挂载到主机目录:
```bash
# 修改 docker-compose.yml 中的 volumes 配置
volumes:
- ./runs:/root/motrixlab/runs
```
## 镜像说明
我们的 Docker 镜像基于 {bdg-primary-line}`NVIDIA CUDA 12.8.1` 运行时环境,预装了以下组件:
- **UV 包管理器**: 快速、可靠的依赖管理
- **SKRL**: 支持 JAX 和 PyTorch 后端的强化学习库
- **TensorBoard**: 训练过程可视化工具
- **MotrixSim**: 高性能物理仿真引擎
- **MotrixLab**: 完整的强化学习训练框架
镜像层构建过程:
1. **基础环境**: NVIDIA CUDA 12.8.1 Runtime + Ubuntu 24.04
2. **系统依赖**: 安装 UV 包管理器和必要的系统工具
3. **Python 依赖**: 使用 UV 缓存机制快速安装 Python 包
4. **项目代码**: 复制 MotrixLab 源代码并完成依赖安装
## 故障排查
### GPU 不可用
如果容器无法访问 GPU
```bash
# 检查 NVIDIA Docker Runtime
docker run --rm --gpus all nvidia/cuda:12.8.1-base-ubuntu24.04 nvidia-smi
# 确认 NVIDIA Container Toolkit 已正确安装
which nvidia-container-cli
```
### 存储空间不足
清理 Docker 缓存和未使用的镜像:
```bash
# 清理构建缓存
docker builder prune
# 删除未使用的镜像
docker image prune -a
# 清理所有未使用的资源
docker system prune -a
```
## 性能优化
### 使用 UV 缓存加速构建
Dockerfile 使用了 UV 的缓存挂载功能,可以显著加速重建过程:
```bash
# 利用 UV 缓存重新构建
docker build --cache-from motphys/motrixlab:latest -t motphys/motrixlab:latest .
```
### GPU 资源分配
您可以在 `docker-compose.yml` 中指定 GPU 使用数量:
```yaml
deploy:
resources:
reservations:
devices:
- driver: nvidia
device_ids: ["0", "1"] # 使用 GPU 0 和 1
capabilities: [gpu]
```
## 下一步
- 查看 [快速入门教程](hello_motrixlab.md) 了解 MotrixLab 基本用法
- 阅读 [训练示例](../demo/cartpole.md) 学习更多训练任务
- 探索 [基础框架](../tutorial/basic_frame.md) 深入理解框架架构

View File

@@ -5,6 +5,7 @@
:maxdepth: 1
getting_started/installation
getting_started/container_deployment
getting_started/hello_motrixlab
```
@@ -25,16 +26,22 @@ tutorial/training_and_result
:caption: 训练示例
:maxdepth: 1
demo/acrobot
demo/cartpole
demo/pendulum
demo/bounce_ball
demo/dm_walker
demo/dm_cheetah
demo/dm_hopper
demo/dm_reacher
demo/dm_finger
demo/dm_humanoid
demo/bring_ball
demo/locomotion_unitree_go1
demo/locomotion_unitree_go1_rough_terrain
demo/anymal_c
demo/franka_lift_cube
demo/franka_open_cabinet
demo/shadow_hand_repose
```

View File

@@ -4,12 +4,12 @@ build-backend = "uv_build"
[project]
name = "motrix-envs"
version = "0.1.0"
version = "0.2.0"
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.5.0b2",
"motrixsim>=0.6.0b1",
]

View File

@@ -13,4 +13,16 @@
# limitations under the License.
# ==============================================================================
from . import bounce_ball, cartpole, cheetah, hopper, reacher, walker # noqa: F401 import to register envs
from . import ( # noqa: F401 import to register envs
acrobot,
bounce_ball,
cartpole,
cheetah,
finger,
hopper,
humanoid,
manipulator,
pendulum,
reacher,
walker,
)

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 acrobot_np # noqa: F401

View File

@@ -0,0 +1,49 @@
<!--
Based on Coulomb's [1] rather than Spong's [2] model.
[1] Coulom, Rémi. Reinforcement learning using neural networks, with applications to motor control.
Diss. Institut National Polytechnique de Grenoble-INPG, 2002.
[2] Spong, Mark W. "The swing up control problem for the acrobot."
IEEE control systems 15, no. 1 (1995): 49-55.
-->
<mujoco model="acrobot">
<include file="../../common/visual.xml"/>
<include file="../../common/materials.xml"/>
<default>
<joint damping="0.05"/>
<geom type="capsule" mass="1"/>
</default>
<option timestep="0.01" integrator="RK4" gravity="0 0 -9.81">
<flag contact="disable" energy="enable"/>
</option>
<asset>
<texture name="skybox" type="skybox" builtin="gradient" rgb1="0.4 0.4 0.4" rgb2="0 0 0"
width="512" height="512"/>
<texture name="motphys-ground" type="2d" file="../../common/motphys-ground.png"/>
<material name="motphys-ground" texture="motphys-ground" texuniform="true" texrepeat="0.4 0.4"/>
</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="motphys-ground" pos="0 0 -1"/>
<site name="target" type="sphere" pos="0 0 4" size="0.2" material="target" group="3"/>
<camera name="fixed" pos="0 -6 2" zaxis="0 -1 0"/>
<camera name="lookat" mode="targetbodycom" target="upper_arm" pos="0 -2 3"/>
<body name="upper_arm" pos="0 0 2">
<joint name="shoulder" type="hinge" axis="0 1 0"/>
<geom name="upper_arm_decoration" material="decoration" type="cylinder" fromto="0 -.06 0 0 .06 0" size="0.051" mass="0"/>
<geom name="upper_arm" fromto="0 0 0 0 0 1" size="0.05" material="self"/>
<body name="lower_arm" pos="0 0 1">
<joint name="elbow" type="hinge" axis="0 1 0"/>
<geom name="lower_arm" fromto="0 0 0 0 0 1" size="0.049" material="self"/>
<site name="tip" pos="0 0 1" size="0.01"/>
</body>
</body>
</worldbody>
<actuator>
<motor name="elbow" joint="elbow" gear="30" ctrllimited="true" ctrlrange="-1 1"/>
</actuator>
</mujoco>

View File

@@ -0,0 +1,152 @@
# 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 import reward
from motrix_envs.np.env import NpEnv, NpEnvState
from .cfg import AcrobotEnvCfg
@registry.env("acrobot", "np")
class AcrobotEnv(NpEnv):
_cfg: AcrobotEnvCfg
def __init__(self, cfg: AcrobotEnvCfg, num_envs: int = 1):
super().__init__(cfg, num_envs=num_envs)
self._action_space = gym.spaces.Box(-1.0, 1.0, (1,), dtype=np.float32)
self._observation_space = gym.spaces.Box(-np.inf, np.inf, (6,), dtype=np.float32)
self._num_dof_pos = self._model.num_dof_pos
self._num_dof_vel = self._model.num_dof_vel
self._tip = self._model.get_site("tip")
self._target = self._model.get_site("target")
self._upper_arm = self._model.get_link("upper_arm")
self._lower_arm = self._model.get_link("lower_arm")
self._target_radius = 0.2
self._step_count = np.zeros(self._num_envs, dtype=np.int32)
self._max_steps = int(cfg.max_episode_seconds / cfg.ctrl_dt)
@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):
actions = np.clip(actions, -1.0, 1.0)
state.data.actuator_ctrls = actions
return state
def _get_obs(self, data: mtx.SceneData) -> np.ndarray:
dof_pos = data.dof_pos
shoulder_angle = dof_pos[:, 0]
elbow_angle = dof_pos[:, 1]
upper_arm_horizontal = np.cos(shoulder_angle)
upper_arm_vertical = np.sin(shoulder_angle)
total_angle = shoulder_angle + elbow_angle
lower_arm_horizontal = np.cos(total_angle)
lower_arm_vertical = np.sin(total_angle)
dof_vel = data.dof_vel
obs = np.concatenate(
[
upper_arm_horizontal.reshape(-1, 1),
lower_arm_horizontal.reshape(-1, 1),
upper_arm_vertical.reshape(-1, 1),
lower_arm_vertical.reshape(-1, 1),
dof_vel,
],
axis=-1,
)
return obs
def update_state(self, state: NpEnvState) -> NpEnvState:
data = state.data
obs = self._get_obs(data)
tip_pos = self._tip.get_pose(data)
target_pos = self._target.get_pose(data)
dist_to_target = np.linalg.norm(tip_pos[:, :3] - target_pos[:, :3], axis=-1)
base_rwd = reward.tolerance(
dist_to_target,
bounds=(0, self._target_radius),
margin=0,
value_at_margin=0.0,
sigmoid="linear",
)
in_target = dist_to_target < self._target_radius
continuous_reward = 0.1 * in_target
distance_reward = 0.3 * (1.0 - np.clip(dist_to_target / 2.0, 0, 1.0))
dof_vel = data.dof_vel
vel_magnitude = np.mean(np.abs(dof_vel), axis=-1)
velocity_penalty = 0.01 * np.maximum(0, vel_magnitude - 2.0)
rwd = base_rwd + continuous_reward + distance_reward - velocity_penalty
self._step_count += 1
terminated = np.zeros((self._num_envs,), dtype=bool)
terminated = np.logical_or(self._step_count >= self._max_steps, terminated)
terminated = np.logical_or(np.isnan(obs).any(axis=-1), terminated)
state.obs = obs
state.reward = rwd
state.terminated = terminated
return state
def reset(self, data: mtx.SceneData) -> tuple[np.ndarray, dict]:
data.reset(self._model)
num_reset = data.shape[0]
shoulder_angle = np.random.uniform(-np.pi, np.pi, size=num_reset).astype(np.float32)
elbow_angle = np.random.uniform(-np.pi, np.pi, size=num_reset).astype(np.float32)
dof_pos = np.stack([shoulder_angle, elbow_angle], axis=-1)
dof_vel = np.zeros((*data.shape, self._num_dof_vel), dtype=np.float32)
data.set_dof_vel(dof_vel)
data.set_dof_pos(dof_pos, self._model)
self._model.forward_kinematic(data)
obs = self._get_obs(data)
return obs, {}
def _reset_done_envs(self):
"""
Reset the environments that are done
"""
super()._reset_done_envs()
done = self._state.done
if np.any(done):
self._step_count[done] = 0

View File

@@ -0,0 +1,33 @@
# 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__) + "/acrobot.xml"
@registry.envcfg("acrobot")
@dataclass
class AcrobotEnvCfg(EnvCfg):
model_file: str = model_file
reset_noise_scale: float = 0.1
max_episode_seconds: float = 10.0
render_spacing: float = 2.0
sim_dt: float = 0.01
ctrl_dt: float = 0.02

View File

@@ -31,8 +31,34 @@
<light pos="0 0 1.5" dir="0 0 -1" directional="true"/>
<geom type="plane" size="0 0 .01" material="motphys-ground"/>
<!-- Target height visualization using mocap body (can be moved at runtime) -->
<body name="target_height_marker" mocap="true" pos="0.58856 0 0.5">
<geom name="target_height_visual"
type="cylinder"
size="0.1 0.002"
rgba="0 1 0 0.5"
contype="0"
conaffinity="0"/>
<geom name="target_center_dot"
type="cylinder"
size="0.01 0.002"
rgba="0 0.6 0 0.9"
contype="0"
conaffinity="0"/>
</body>
<!-- Paddle home position marker -->
<body name="paddle_home_marker" mocap="true" pos="0.5857 -0.0082 0.2">
<geom name="paddle_home_visual"
type="box"
size="0.1 0.1 0.003"
rgba="1 1 0.6 0.4"
contype="0"
conaffinity="0"/>
</body>
<geom type="mesh" rgba="0.25098 0.25098 0.25098 1" mesh="base_link" class="visual"/>
<body name="Link1" pos="0 0 0.1">
<body name="Link1" pos="0 0 0.33">
<inertial pos="0.00022014 -7.0626e-06 -0.10379" quat="0.998051 -0.0622621 0.00360518 0.00215072" mass="0.98482" diaginertia="0.00658905 0.00502396 0.00428069"/>
<joint name="Joint1" pos="0 0 0" axis="0 0 1" range="-2.96706 2.96706" actuatorfrcrange="-300 300"/>
<geom type="mesh" rgba="1 0.69804 0 1" mesh="Link1" class="visual"/>
@@ -68,7 +94,7 @@
<body name="ball_link" pos="0 0 0.07">
<freejoint/>
<inertial pos="0 0 0" mass="0.0027" diaginertia="1 1 1"/>
<geom size="0.019" contype="1" rgba="0 1 0 1" solref="1 0"/>
<geom size="0.019" contype="1" rgba="1 0 0 1" solref="1 0"/>
</body>
</worldbody>

View File

@@ -30,36 +30,71 @@ class BounceBallEnv(NpEnv):
def __init__(self, cfg: BounceBallEnvCfg, num_envs: int = 1):
super().__init__(cfg, num_envs=num_envs)
# Action space: 6D normalized paddle velocity (dx, dy, dz, dr_x, dr_y, dr_z)
# Action space: 6D joint position control
self._action_space = gym.spaces.Box(-1.0, 1.0, (6,), dtype=np.float32)
# Observation space: simplified version using only DOF information
# DOF pos (13) + DOF vel (12) = 25 (this includes ball state implicitly)
self._observation_space = gym.spaces.Box(-np.inf, np.inf, (25,), dtype=np.float32)
# Observation space: joint states + paddle position + target height (29D)
self._observation_space = gym.spaces.Box(-np.inf, np.inf, (29,), dtype=np.float32)
self._num_dof_pos = self._model.num_dof_pos
self._num_dof_vel = self._model.num_dof_vel
# Initial arm joint positions (degrees converted to radians)
# Initial arm joint positions
self._init_arm_qpos = np.array(self._cfg.arm_init_qpos, dtype=np.float32) * np.pi / 180.0
self._init_dof_vel = np.zeros(self._model.num_dof_vel, dtype=np.float32)
# Initialize full DOF positions (6 arm joints + 7 for ball free joint)
# Full DOF positions (6 arm joints + 7 ball free joint)
self._init_dof_pos = np.zeros(self._model.num_dof_pos, dtype=np.float32)
self._init_dof_pos[:6] = self._init_arm_qpos
# Get body and geom IDs
self._paddle_geom_id = self._model.geom_names.index("blocker")
# Body and geom references
self._paddle_geom = self._model.get_geom("blocker")
self._ball_body_id = self._model.body_names.index("ball_link")
# Action scaling parameters
# Mocap bodies for visual markers
self._target_marker_body = self._model.get_body("target_height_marker")
assert self._target_marker_body.is_mocap, "target_height_marker must be a mocap body"
self._paddle_home_marker_body = self._model.get_body("paddle_home_marker")
assert self._paddle_home_marker_body.is_mocap, "paddle_home_marker must be a mocap body"
# Action scaling
self._action_scale = np.array(self._cfg.action_scale, dtype=np.float32)
self._action_bias = np.array(self._cfg.action_bias, dtype=np.float32)
# Track ball initial position for reset
# Ball initial conditions
self._ball_init_pos = np.array(self._cfg.ball_init_pos, dtype=np.float32)
self._ball_init_vel = np.array(self._cfg.ball_init_vel, dtype=np.float32)
# Constants for marker poses
self._ball_radius = 0.019 # Ball radius in meters
# Target marker base pose: [x, y, z_placeholder, qx, qy, qz, qw]
self._target_marker_base_pose = np.array(
[
self._cfg.target_ball_x,
self._cfg.target_ball_y,
0.0, # z will be set per environment
0.0,
0.0,
0.0,
1.0, # identity quaternion
],
dtype=np.float32,
)
# Paddle home marker pose: [x, y, z, qx, qy, qz, qw]
self._paddle_home_marker_pose = np.array(
[
self._cfg.target_ball_x,
self._cfg.target_ball_y,
self._cfg.paddle_home_position_z,
0.0,
0.0,
0.0,
1.0, # identity quaternion
],
dtype=np.float32,
)
@property
def observation_space(self):
return self._observation_space
@@ -69,118 +104,361 @@ class BounceBallEnv(NpEnv):
return self._action_space
def _denormalize_action(self, action: np.ndarray) -> np.ndarray:
"""Denormalize action to get actual paddle velocity changes"""
"""Denormalize action from [-1, 1] to joint position changes"""
return self._action_scale * action + self._action_bias
def _compute_observation(self, data: mtx.SceneData) -> np.ndarray:
"""Compute 25-dimensional observation vector from DOF states"""
# Use DOF positions and velocities directly
def _compute_observation(self, data: mtx.SceneData, target_heights: np.ndarray) -> np.ndarray:
"""Compute observation: joint states + paddle position + target height (29D)"""
dof_pos = data.dof_pos
dof_vel = data.dof_vel
# Concatenate DOF positions (13) and velocities (12)
obs = np.concatenate([dof_pos, dof_vel], axis=-1)
# Get paddle position
paddle_pose = self._paddle_geom.get_pose(data)
paddle_xyz = paddle_pose[:, :3]
# Concatenate: DOF pos (13) + DOF vel (12) + paddle xyz (3) + target height (1)
obs = np.concatenate([dof_pos, dof_vel, paddle_xyz, target_heights[:, np.newaxis]], axis=-1)
return obs.astype(np.float32)
def _compute_reward(
self, obs: np.ndarray, data: mtx.SceneData = None, consecutive_bounces: np.ndarray = None
) -> np.ndarray:
"""Compute reward based on ball height, position, and controlled upward velocity"""
# Extract ball position and velocity from DOF
ball_x = obs[:, 6] # Ball x position
ball_z = obs[:, 8] # Ball z position
self,
obs: np.ndarray,
data: mtx.SceneData = None,
consecutive_bounces: np.ndarray = None,
bounce_detected: np.ndarray = None,
target_heights: np.ndarray = None,
current_actions: np.ndarray = None,
last_actions: np.ndarray = None,
) -> tuple:
"""
Compute reward based on ball position, velocity, and paddle alignment.
ball_vz = obs[:, 13 + 8] # Ball z velocity (13 pos + 8 vel)
The reward function uses a composite design with multiple reward and penalty terms
to guide the robot to learn a stable ball bouncing strategy.
Returns:
tuple: (total_reward, reward_details) where reward_details contains
individual reward components for analysis.
"""
# Extract ball state
ball_x = obs[:, 6]
ball_y = obs[:, 7]
ball_z = obs[:, 8]
ball_vz = obs[:, 13 + 8]
# Extract paddle position
paddle_xy = obs[:, 25:27]
paddle_z = obs[:, 27]
# Target positions
target_ball_x = 0.58856 # Target x position
target_height = self._cfg.target_ball_height
tolerance = self._cfg.height_tolerance
target_ball_x = self._cfg.target_ball_x
target_ball_y = self._cfg.target_ball_y
target_height = target_heights
# 1. Position control reward - MOST IMPORTANT for keeping ball centered
# Strong reward for ball being at the right x position (paddle center)
# Physics constant
g = self._cfg.gravity
# ============================================================================
# 1. Horizontal position reward (weighted by vertical distance)
# Core reward ensuring ball stays directly above paddle
# ============================================================================
x_position_error = np.abs(ball_x - target_ball_x)
x_position_reward = np.exp(-(x_position_error**2) / (2 * 0.05**2)) # Tight tolerance for x position
y_position_error = np.abs(ball_y - target_ball_y)
xy_position_error = np.sqrt(x_position_error**2 + y_position_error**2)
# 2. Height-based reward - less important than position control
vertical_dist = np.abs(ball_z - paddle_z)
vertical_weight = np.exp(-vertical_dist / self._cfg.vertical_weight_scale)
weighted_horizontal_scale = self._cfg.weighted_horizontal_base_scale * (
1.0 + self._cfg.weighted_horizontal_weight_factor * vertical_weight
)
weighted_position_reward = np.exp(-(xy_position_error**2) / (2 * weighted_horizontal_scale**2))
# ============================================================================
# 2. Out-of-position penalty
# Strong penalty for severe deviation to prevent ball flying out of control
# ============================================================================
out_of_position_penalty = -2.0 / (
1.0
+ np.exp(-(xy_position_error - self._cfg.out_of_position_threshold) / self._cfg.out_of_position_sharpness)
)
# ============================================================================
# 3. Velocity matching reward
# Based on projectile motion physics, encourages ball trajectory to have
# desired velocity at target height for stable control
# ============================================================================
height_diff = target_height - ball_z
desired_velocity_at_target = self._cfg.desired_velocity_at_target
# Calculate velocity at target height using physics
upward_below_condition = (ball_vz > 0) & (ball_z < target_height)
velocity_squared_at_target_upward = np.where(upward_below_condition, ball_vz**2 - 2 * g * height_diff, 0.0)
velocity_at_target_upward = np.sqrt(np.maximum(0, velocity_squared_at_target_upward))
downward_above_condition = (ball_vz < 0) & (ball_z > target_height)
velocity_squared_at_target_downward = np.where(
downward_above_condition, ball_vz**2 + 2 * g * np.abs(height_diff), 0.0
)
velocity_at_target_downward = -np.sqrt(np.maximum(0, velocity_squared_at_target_downward))
near_target_condition = np.abs(height_diff) < 0.05
velocity_at_target_near = np.where(near_target_condition, ball_vz, 0.0)
# Smooth combination
upward_motion = 1.0 / (1.0 + np.exp(-ball_vz / 0.2))
below_target = 1.0 / (1.0 + np.exp(-height_diff / 0.02))
downward_motion = 1.0 - upward_motion
above_target = 1.0 - below_target
at_target_weight = np.exp(-(height_diff**2) / (2 * 0.01**2))
velocity_at_target = (
velocity_at_target_upward * upward_motion * below_target
+ velocity_at_target_downward * downward_motion * above_target
+ velocity_at_target_near * at_target_weight
)
velocity_error = np.abs(velocity_at_target - desired_velocity_at_target)
velocity_matching_reward = np.exp(-(velocity_error**2) / (2 * self._cfg.velocity_error_sigma**2))
# ============================================================================
# 4. Height reward
# Directly encourages ball to approach target height, core task objective
# ============================================================================
height_error = np.abs(ball_z - target_height)
height_reward = np.exp(-(height_error**2) / (2 * tolerance**2))
height_reward = np.exp(-(height_error**2) / (2 * self._cfg.height_error_sigma**2))
# 3. Controlled upward velocity reward - only when ball is in good position
# Only reward upward velocity when ball is well-positioned horizontally
well_positioned = x_position_error < 0.02 # Ball must be very close to target x
controlled_upward_reward = np.where(
well_positioned & (ball_vz > 0.1) & (ball_vz < 1.5), # Reasonable upward velocity
np.clip(ball_vz * 1.5, 0.0, 1.5), # Reduced scale
0.0,
# Height progress bonus
height_progress_bonus = (
np.maximum(0, ball_z - self._cfg.height_progress_threshold) * self._cfg.height_progress_scale
)
# 4. Strong penalty for being out of position horizontally
out_of_position_penalty = np.where(
x_position_error > 0.1,
-2.0, # Heavy penalty for being far from center
0.0,
# ============================================================================
# 5. Controlled upward velocity reward
# Only rewards upward velocity when ball position is good, avoiding random hitting
# Guides strategy to learn precise hitting force
# ============================================================================
positioning_quality = np.exp(-(xy_position_error**2) / (2 * self._cfg.positioning_quality_sigma**2))
ideal_launch_velocity = np.sqrt(2 * g * np.maximum(0, height_diff))
ideal_launch_velocity = np.clip(
ideal_launch_velocity, self._cfg.ideal_velocity_min, self._cfg.ideal_velocity_max
)
# 5. Velocity penalties - discourage excessive speeds
excessive_upward_penalty = np.where(ball_vz > 2.0, -1.0, 0.0)
downward_velocity_penalty = np.where(ball_vz < -0.5, -np.clip(-ball_vz * 0.3, 0.0, 0.5), 0.0)
# 6. Position-based penalties (reduced)
overshoot_penalty = np.where(ball_z > target_height + tolerance, -0.3, 0.0)
undershoot_penalty = np.where(ball_z < 0.1, -0.5, 0.0)
# 7. Consecutive bounces reward - only when position is good
good_position_for_bounce = x_position_error < 0.05 # Reasonable position for bouncing
consecutive_bounces_reward = np.where(
good_position_for_bounce & (consecutive_bounces > 0),
np.log(consecutive_bounces + 1) * 0.3, # Reduced scale
0.0,
upward_velocity_quality = np.exp(
-((ball_vz - ideal_launch_velocity) ** 2) / (2 * self._cfg.upward_velocity_sigma**2)
)
# Bonus for high bounce counts (only when well-positioned)
high_bounce_bonus = np.where(
good_position_for_bounce & (consecutive_bounces >= 3),
consecutive_bounces * 0.1, # Reduced bonus
0.0,
upward_mask = 1.0 / (1.0 + np.exp(-ball_vz / self._cfg.upward_mask_scale))
controlled_upward_reward = (
positioning_quality
* upward_velocity_quality
* upward_mask
* np.clip(ball_vz * 1.0, 0.0, self._cfg.controlled_upward_clip_max)
)
# Combine all rewards with corrected priorities
# ============================================================================
# 6. Velocity penalties
# Prevents ball velocity from being too fast or falling freely
# Ensures ball motion stays within controllable range
# ============================================================================
excessive_upward_penalty = -1.0 / (
1.0 + np.exp(-(ball_vz - self._cfg.excessive_upward_threshold) / self._cfg.excessive_upward_sharpness)
)
downward_penalty_magnitude = -ball_vz * np.clip(
-ball_vz * self._cfg.downward_velocity_scale, 0.0, self._cfg.downward_velocity_clip_max
)
downward_penalty_trigger = 1.0 / (1.0 + np.exp((ball_vz - self._cfg.downward_velocity_threshold) / 0.2))
downward_velocity_penalty = downward_penalty_magnitude * downward_penalty_trigger
# ============================================================================
# 7. Consecutive bounces reward
# Encourages multiple consecutive successful bounces for stable long-term control
# Uses logarithmic function to avoid infinite reward growth
# ============================================================================
bounce_positioning_quality = np.exp(-(xy_position_error**2) / (2 * self._cfg.bounce_positioning_sigma**2))
bounce_log_reward = np.log(consecutive_bounces.astype(np.float32) + 1.0) * self._cfg.bounce_log_scale
bounce_activation = (consecutive_bounces > 0).astype(np.float32) * bounce_positioning_quality
consecutive_bounces_reward = bounce_log_reward * bounce_activation
# High bounce count bonus
high_bounce_activation = 1.0 / (
1.0
+ np.exp(
-(consecutive_bounces.astype(np.float32) - self._cfg.high_bounce_threshold)
/ self._cfg.high_bounce_sharpness
)
)
high_bounce_bonus = (
consecutive_bounces.astype(np.float32)
* self._cfg.high_bounce_scale
* bounce_positioning_quality
* high_bounce_activation
)
# ============================================================================
# 8. Paddle-ball horizontal alignment
# Encourages paddle to actively move directly below ball
# Extra reward at bounce moment to reinforce correct hitting behavior
# ============================================================================
if bounce_detected is None:
bounce_detected = np.zeros(ball_x.shape[0], dtype=bool)
ball_xy = np.stack([ball_x, ball_y], axis=1)
paddle_ball_xy_error = np.linalg.norm(ball_xy - paddle_xy, axis=1)
vertical_proximity_weight = np.exp(-vertical_dist / self._cfg.vertical_proximity_scale)
paddle_alignment_quality = np.exp(-(paddle_ball_xy_error**2) / (2 * self._cfg.paddle_alignment_sigma**2)) * (
1.0 + self._cfg.paddle_alignment_weight_factor * vertical_proximity_weight
)
bounce_boost = bounce_detected.astype(np.float32) * self._cfg.bounce_boost_factor + 1.0
paddle_center_reward = paddle_alignment_quality * bounce_boost * self._cfg.paddle_center_scale
# ============================================================================
# 9. Paddle home position reward
# Encourages paddle to return to home position when ball is far away
# Makes paddle motion more energy-efficient and natural
# ============================================================================
paddle_home_z = self._cfg.paddle_home_position_z
paddle_height_deviation = np.abs(paddle_z - paddle_home_z)
# Distance-based dynamic factor
distance_factor = 1.0 + self._cfg.distance_factor_scale / (
1.0 + np.exp(-(vertical_dist - self._cfg.distance_factor_threshold) / self._cfg.distance_factor_sharpness)
)
home_position_reward = (
np.exp(-(paddle_height_deviation**2) / (2 * self._cfg.home_position_sigma**2)) * distance_factor
)
# Height violation penalty
max_deviation = self._cfg.max_paddle_height_deviation
height_violation = np.maximum(0, paddle_height_deviation - max_deviation)
height_violation_penalty = -height_violation * self._cfg.height_violation_scale
# ============================================================================
# 10. Action and velocity penalties
# Penalizes drastic action changes and excessive joint velocities
# Encourages smooth and energy-efficient control
# ============================================================================
num_envs = obs.shape[0]
if current_actions is None:
current_actions = np.zeros((num_envs, 6), dtype=np.float32)
if last_actions is None:
last_actions = np.zeros((num_envs, 6), dtype=np.float32)
action_diff = current_actions - last_actions
action_penalty = np.sum(np.square(action_diff), axis=-1)
joint_vel = data.dof_vel[:, :6]
joint_vel_penalty = np.sum(np.square(joint_vel), axis=-1)
# ============================================================================
# Total reward
# ============================================================================
action_penalty_rate = self._cfg.action_penalty_rate
joint_vel_penalty_rate = self._cfg.joint_vel_penalty_rate
total_reward = (
x_position_reward * 2.0 # X position control (200%) - MOST IMPORTANT
+ controlled_upward_reward * 1.0 # Controlled upward velocity (100%)
+ height_reward * 0.3 # Height accuracy (30%) - less important
+ consecutive_bounces_reward * 1.0 # Consecutive bounces (100%) - reduced
+ high_bounce_bonus * 0.3 # High bounce bonus (30%) - reduced
+ out_of_position_penalty * 1.0 # Out of position penalty (100%)
+ excessive_upward_penalty * 1.0 # Excessive upward penalty (100%)
+ downward_velocity_penalty * 1.0 # Downward penalty (100%)
+ overshoot_penalty # Height overshoot penalty
+ undershoot_penalty # Height undershoot penalty
weighted_position_reward * self._cfg.weighted_position_weight
+ velocity_matching_reward * self._cfg.velocity_matching_weight
+ height_reward * self._cfg.height_reward_weight
+ height_progress_bonus * self._cfg.height_progress_weight
+ controlled_upward_reward * self._cfg.controlled_upward_weight
+ consecutive_bounces_reward * self._cfg.consecutive_bounces_weight
+ high_bounce_bonus * self._cfg.high_bounce_weight
+ paddle_center_reward * self._cfg.paddle_center_weight
+ home_position_reward * self._cfg.home_position_weight
+ out_of_position_penalty * self._cfg.out_of_position_weight
+ excessive_upward_penalty * self._cfg.excessive_upward_weight
+ downward_velocity_penalty * self._cfg.downward_velocity_weight
+ height_violation_penalty * self._cfg.height_violation_weight
- action_penalty_rate * action_penalty
- joint_vel_penalty_rate * joint_vel_penalty
)
return total_reward
# ============================================================================
# Reward details for debugging
# ============================================================================
reward_details = {
"x_position_error": x_position_error,
"y_position_error": y_position_error,
"xy_position_error": xy_position_error,
"ball_z": ball_z,
"paddle_z": paddle_z,
"vertical_dist": vertical_dist,
"vertical_weight": vertical_weight,
"ball_vz": ball_vz,
"height_diff": height_diff,
"height_error": height_error,
"velocity_at_target": velocity_at_target,
"desired_velocity_at_target": np.full_like(ball_vz, desired_velocity_at_target),
"velocity_error": velocity_error,
"ideal_launch_velocity": ideal_launch_velocity,
"positioning_quality": positioning_quality,
"upward_velocity_quality": upward_velocity_quality,
"bounce_positioning_quality": bounce_positioning_quality,
"paddle_ball_xy_error": paddle_ball_xy_error,
"vertical_proximity_weight": vertical_proximity_weight,
"bounce_detected": bounce_detected.astype(np.float32),
"consecutive_bounces": consecutive_bounces.astype(np.float32),
"action_penalty": action_penalty,
"joint_vel_penalty": joint_vel_penalty,
"distance_factor": distance_factor,
"weighted_position_reward": weighted_position_reward * 2.0,
"velocity_matching_reward": velocity_matching_reward * 2.0,
"height_reward": height_reward * 4.5,
"height_progress_bonus": height_progress_bonus * 1.0,
"controlled_upward_reward": controlled_upward_reward * 1.5,
"consecutive_bounces_reward": consecutive_bounces_reward * 0.8,
"high_bounce_bonus": high_bounce_bonus * 0.3,
"paddle_center_reward": paddle_center_reward * 0.6,
"home_position_reward": home_position_reward * 1.5,
"out_of_position_penalty": out_of_position_penalty * 1.0,
"excessive_upward_penalty": excessive_upward_penalty * 1.0,
"downward_velocity_penalty": downward_velocity_penalty * 1.0,
"height_violation_penalty": height_violation_penalty * 1.0,
"action_penalty_weighted": -action_penalty_rate * action_penalty,
"joint_vel_penalty_weighted": -joint_vel_penalty_rate * joint_vel_penalty,
"paddle_height_deviation": paddle_height_deviation,
"total_reward": total_reward,
}
def _compute_terminated(self, obs: np.ndarray) -> np.ndarray:
return total_reward, reward_details
def _compute_terminated(self, obs: np.ndarray, target_heights: np.ndarray) -> np.ndarray:
"""Check if episode should terminate based on DOF states"""
# Extract ball position from DOF (indices 6-8 for x,y,z)
ball_x = obs[:, 6] # Ball x position
ball_y = obs[:, 7] # Ball y position
ball_z = obs[:, 8] # Ball z position
# Target height from config
target_height = self._cfg.target_ball_height
# Terminate if ball falls below ground or goes significantly higher than target
terminated = (ball_z < 0.05) | (ball_z > target_height + 1.0)
terminated = (ball_z < 0.05) | (ball_z > target_heights + 1.0)
# Also terminate if ball goes too far horizontally
terminated |= np.abs(ball_x) > 1.5
terminated |= (np.abs(ball_x) > 1.5) | (np.abs(ball_y) > 1.5)
# Terminate if joint velocity is too high
# Limit: 360 degrees/second = 2*pi rad/s ≈ 6.28 rad/s
joint_vel = obs[:, 13:19] # Joint velocities (indices 13-18 for 6 arm joints)
max_joint_vel = 2.0 * np.pi # 360 degrees/second in radians
terminated |= np.abs(joint_vel).max(axis=-1) > max_joint_vel
return terminated
def apply_action(self, actions: np.ndarray, state: NpEnvState) -> NpEnvState:
"""Apply action to control paddle position"""
# Store last actions for penalty calculation
state.info["last_actions"] = state.info.get("current_actions", np.zeros_like(actions))
state.info["current_actions"] = actions
# Get current joint positions
current_joint_pos = state.data.dof_pos[:, :6] # First 6 DOFs are arm joints
@@ -198,12 +476,15 @@ class BounceBallEnv(NpEnv):
"""Update state with new observations, rewards, and termination flags"""
data = state.data
# Compute observation
obs = self._compute_observation(data)
# Get bounce tracking from info
# Get bounce tracking and target heights from info
consecutive_bounces = state.info.get("consecutive_bounces", np.zeros(data.shape[0], dtype=np.int32))
ball_was_upward = state.info.get("ball_was_upward", np.zeros(data.shape[0], dtype=bool))
# Use mean of target_height_range as fallback
default_height = np.mean(self._cfg.target_height_range)
target_heights = state.info.get("target_heights", np.full(data.shape[0], default_height, dtype=np.float32))
# Compute observation with target heights
obs = self._compute_observation(data, target_heights)
# Detect bounces and update consecutive bounce count
current_ball_z = obs[:, 8] # Ball z position
@@ -239,19 +520,42 @@ class BounceBallEnv(NpEnv):
normalized_obs = obs
# Compute reward and termination
reward = self._compute_reward(obs, data, consecutive_bounces)
terminated = self._compute_terminated(obs)
reward, reward_details = self._compute_reward(
obs,
data,
consecutive_bounces,
bounce_detected=bounce_detected,
target_heights=target_heights,
current_actions=state.info.get("current_actions"),
last_actions=state.info.get("last_actions"),
)
terminated = self._compute_terminated(obs, target_heights=target_heights)
state.obs = normalized_obs
state.reward = reward
state.terminated = terminated
# Store reward details for debugging
if self._cfg.store_reward_details:
state.info["Reward"] = reward_details
state.info["target_heights"] = target_heights # Ensure target_heights persists across steps
return state
def reset(self, data: mtx.SceneData) -> tuple:
"""Reset environment to initial state"""
"""Reset environment to initial state with randomized target heights"""
cfg: BounceBallEnvCfg = self._cfg
num_reset = data.shape[0]
# Randomize target heights for the environments being reset
if cfg.randomize_target_height:
min_height, max_height = cfg.target_height_range
new_target_heights = np.random.uniform(min_height, max_height, num_reset).astype(np.float32)
else:
# Use mean of target_height_range when not randomizing
default_height = np.mean(cfg.target_height_range)
new_target_heights = np.full(num_reset, default_height, dtype=np.float32)
# Add noise to initial arm joint positions only (not ball)
arm_noise_pos = np.random.uniform(
-cfg.reset_noise_scale,
@@ -267,42 +571,56 @@ class BounceBallEnv(NpEnv):
# Reset simulation first to get proper DOF structure
data.reset(self._model)
# Get current DOF positions and modify only the arm joints
# Get current DOF positions
current_dof_pos = data.dof_pos
current_dof_vel = data.dof_vel
# === Modify all DOF positions ===
# Set arm joint positions (first 6 DOFs)
current_dof_pos[:, :6] = np.tile(self._init_arm_qpos, (num_reset, 1)) + arm_noise_pos
current_dof_vel[:, :6] = noise_vel[:, :6]
# Set the quaternion part properly (DOFs 9-12 are quaternion w,x,y,z for freejoint)
# The ball has a freejoint which uses quaternion representation
for i in range(num_reset):
# Set quaternion for ball (indices 9-12: w, x, y, z)
current_dof_pos[i, 9:13] = [1.0, 0.0, 0.0, 0.0] # Identity quaternion
data.set_dof_pos(current_dof_pos, self._model)
data.set_dof_vel(current_dof_vel)
# Set ball position in DOF (indices 6-8 for x, y, z positions)
for i in range(num_reset):
ball_noise_pos = np.random.uniform(-0.01, 0.01, 3)
ball_noise_pos = np.random.uniform(-0.01, 0.01, (num_reset, 3))
ball_pos = self._ball_init_pos + ball_noise_pos
# Set ball position in DOF coordinates (indices 6-8)
current_dof_pos[i, 6:9] = ball_pos
current_dof_pos[:, 6:9] = ball_pos
# Final update to set both ball position and quaternion
# Apply all DOF position changes
data.set_dof_pos(current_dof_pos, self._model)
# Get current DOF velocities
current_dof_vel = data.dof_vel
# === Modify all DOF velocities ===
# Set arm joint velocities (first 6 DOFs)
current_dof_vel[:, :6] = noise_vel[:, :6]
# Apply ball linear velocity in DOF
data.set_dof_vel(current_dof_vel)
# Update target height marker position (thin cylinder disc)
# Marker center aligns with ball top: marker_z = target_height + ball_radius
target_marker_poses = np.tile(self._target_marker_base_pose, (num_reset, 1))
target_marker_poses[:, 2] = new_target_heights + self._ball_radius # Set z position
self._target_marker_body.mocap.set_pose(data, target_marker_poses)
# Update paddle home marker position
paddle_home_marker_poses = np.tile(self._paddle_home_marker_pose, (num_reset, 1))
# Set paddle home marker mocap body pose
self._paddle_home_marker_body.mocap.set_pose(data, paddle_home_marker_poses)
# Initialize info dict with bounce tracking variables
info = {
"consecutive_bounces": np.zeros(num_reset, dtype=np.int32),
"ball_was_upward": np.zeros(num_reset, dtype=bool),
"max_consecutive_bounces": 0,
"target_heights": new_target_heights.copy(), # Return target heights for this reset batch
"current_actions": np.zeros((num_reset, 6), dtype=np.float32),
"last_actions": np.zeros((num_reset, 6), dtype=np.float32),
}
# Compute initial observation
obs = self._compute_observation(data)
# Compute initial observation with target heights
obs = self._compute_observation(data, new_target_heights)
normalized_obs = obs # No normalization for now
return normalized_obs, info

View File

@@ -42,19 +42,110 @@ class BounceBallEnvCfg(EnvCfg):
ball_init_vel: list = None
arm_init_qpos: list = None
# Target positions
target_ball_x: float = 0.58856 # Target x position (m)
target_ball_y: float = 0.0 # Target y position (m)
# Target height for bouncing (configurable parameter)
target_ball_height: float = 0.8 # Default target height in meters
target_height_range: tuple = (0.53, 0.83) # Range for random target height in meters
randomize_target_height: bool = True # Whether to randomize target height on reset
height_tolerance: float = 0.1 # Tolerance for reward calculation
# Paddle behavior constraints
paddle_home_position_z: float = 0.28 # Home position height for paddle (m)
max_paddle_height_deviation: float = 0.1 # Max deviation from home position (m)
encourage_return_home: bool = True # Encourage paddle to return to home position
encourage_impact_velocity: bool = True # Encourage high upward velocity at impact
# Physics constants
gravity: float = 9.81 # Gravity acceleration (m/s^2)
# Reward function parameters
# Position reward
vertical_weight_scale: float = 0.15 # Scale for vertical distance weight
weighted_horizontal_base_scale: float = 0.1 # Base scale for horizontal position
weighted_horizontal_weight_factor: float = 3.0 # Weight factor for vertical proximity
# Out of position penalty
out_of_position_threshold: float = 0.05 # Threshold distance (m)
out_of_position_sharpness: float = 0.03 # Sigmoid sharpness
# Velocity matching
desired_velocity_at_target: float = 0.5 # Target velocity at target height (m/s)
velocity_error_sigma: float = 0.8 # Sigma for velocity error Gaussian
# Height reward
height_error_sigma: float = 0.15 # Sigma for height error Gaussian
height_progress_scale: float = 2.0 # Scale for height progress bonus
height_progress_threshold: float = 0.2 # Minimum height for progress bonus (m)
# Controlled upward velocity
positioning_quality_sigma: float = 0.02 # Sigma for positioning quality
ideal_velocity_min: float = 0.5 # Minimum ideal launch velocity (m/s)
ideal_velocity_max: float = 3.0 # Maximum ideal launch velocity (m/s)
upward_velocity_sigma: float = 0.5 # Sigma for upward velocity quality
upward_mask_scale: float = 0.1 # Scale for upward mask sigmoid
controlled_upward_clip_max: float = 1.5 # Max clip value for controlled upward
# Velocity penalties
excessive_upward_threshold: float = 3.5 # Threshold for excessive upward velocity (m/s)
excessive_upward_sharpness: float = 0.3 # Sigmoid sharpness
downward_velocity_threshold: float = -0.2 # Threshold for downward penalty (m/s)
downward_velocity_scale: float = 0.3 # Scale for downward penalty magnitude
downward_velocity_clip_max: float = 0.5 # Max clip for downward penalty
# Bounce rewards
bounce_positioning_sigma: float = 0.05 # Sigma for bounce positioning quality
bounce_log_scale: float = 0.5 # Scale for logarithmic bounce reward
high_bounce_threshold: float = 2.0 # Threshold for high bounce bonus
high_bounce_sharpness: float = 0.5 # Sigmoid sharpness for high bounce
high_bounce_scale: float = 0.15 # Scale for high bounce bonus
# Paddle alignment
paddle_alignment_sigma: float = 0.03 # Sigma for paddle-ball alignment
vertical_proximity_scale: float = 0.1 # Scale for vertical proximity weight
paddle_alignment_weight_factor: float = 2.0 # Weight factor for vertical proximity
bounce_boost_factor: float = 2.0 # Boost factor when bounce detected
paddle_center_scale: float = 0.3 # Overall scale for paddle center reward
# Home position reward
home_position_sigma: float = 0.05 # Sigma for home position deviation
distance_factor_threshold: float = 0.15 # Threshold for distance factor (m)
distance_factor_sharpness: float = 0.03 # Sigmoid sharpness
distance_factor_scale: float = 0.5 # Scale for distance factor
height_violation_scale: float = 20.0 # Scale for height violation penalty
# Action and velocity penalties
action_penalty_rate: float = 1e-4 # Penalty rate for action changes
joint_vel_penalty_rate: float = 1e-4 # Penalty rate for joint velocities
# Reward weights
weighted_position_weight: float = 2.0
velocity_matching_weight: float = 2.0
height_reward_weight: float = 4.5
height_progress_weight: float = 1.0
controlled_upward_weight: float = 1.5
consecutive_bounces_weight: float = 0.8
high_bounce_weight: float = 0.3
paddle_center_weight: float = 0.6
home_position_weight: float = 1.5
out_of_position_weight: float = 1.0
excessive_upward_weight: float = 1.0
downward_velocity_weight: float = 1.0
height_violation_weight: float = 1.0
# Action scaling parameters
action_scale: list = None
action_bias: list = None
# Debug options
store_reward_details: bool = False # Whether to store detailed reward breakdown in state.info, it's very slow
def __post_init__(self):
if self.ball_init_pos is None:
self.ball_init_pos = [0.58856, 0, 1.27796] # Slightly above paddle (paddle z=0.2803)
self.ball_init_pos = [0.58856, 0, 0.68]
if self.ball_init_vel is None:
self.ball_init_vel = [0.0, 0.0, 0.0]
self.ball_init_vel = [0.0, 0.0, -0.2]
if self.arm_init_qpos is None:
self.arm_init_qpos = [0, 40, 110, 0, -60, 0]

View File

@@ -30,4 +30,4 @@ class CheetahEnvCfg(EnvCfg):
render_spacing: float = 2.0
sim_dt: float = 0.01
ctrl_dt: float = 0.025
run_speed: float = 30.0
run_speed: float = 10.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 finger_np # noqa: F401

View File

@@ -0,0 +1,102 @@
# 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__) + "/finger.xml"
spin_model_file = os.path.dirname(__file__) + "/finger_spin.xml"
turn_easy_model_file = os.path.dirname(__file__) + "/finger_turn_easy.xml"
turn_hard_model_file = os.path.dirname(__file__) + "/finger_turn_hard.xml"
@dataclass
class FingerBaseCfg(EnvCfg):
model_file: str = model_file
max_episode_seconds: float = 20.0
sim_dt: float = 0.01
ctrl_dt: float = 0.02
# Task setup
task: str = "spin" # "spin" | "turn"
target_radius: float = 0.07
# Reward thresholds (match dm_control defaults)
spin_velocity_threshold: float = 15.0
# Reward mode
# - "sparse": match dm_control (1 if hinge_velocity <= -threshold else 0)
# - "shaped": dense reward to make training easier
reward_mode: str = "sparse"
shaped_reward_beta: float = 1.0
# Extra shaping for Spin tasks (helps reduce "no contact" failures)
spin_touch_bonus_scale: float = 0.0
spin_touch_bonus_tanh_scale: float = 50.0
spin_approach_reward_scale: float = 0.0
spin_approach_sigma: float = 0.15
# Turn shaping: reward falls linearly to 0 at margin = scale * target_radius
turn_reward_margin_scale: float = 4.0
turn_reward_min_margin: float = 0.0
turn_shaped_reward_beta: float = 1.0
# Turn shaping mode:
# - "linear": clip(1 - max(dist,0)/margin, 0..1)
# - "exp": exp(-max(dist,0)/sigma)
turn_reward_shape: str = "linear"
turn_reward_sigma_scale: float = 1.0
turn_reward_sigma_min: float = 0.05
# Extra shaping for Turn tasks (to reduce jitter and help contact)
turn_touch_bonus_scale: float = 0.05
turn_touch_bonus_tanh_scale: float = 50.0
# Encourage approaching the spinner (helps avoid "no contact" deadlock)
turn_approach_reward_scale: float = 0.3
turn_approach_sigma: float = 0.15
turn_action_l2_penalty_scale: float = 0.002
turn_action_delta_l2_penalty_scale: float = 0.01
# Reset sampling
reset_collision_free_attempts: int = 200
@registry.envcfg("dm-finger-spin")
@dataclass
class FingerSpinCfg(FingerBaseCfg):
model_file: str = spin_model_file
task: str = "spin"
reward_mode: str = "shaped"
spin_approach_reward_scale: float = 0.15
spin_touch_bonus_scale: float = 0.03
@registry.envcfg("dm-finger-turn-easy")
@dataclass
class FingerTurnEasyCfg(FingerBaseCfg):
model_file: str = turn_easy_model_file
task: str = "turn"
target_radius: float = 0.07
reward_mode: str = "shaped"
turn_reward_shape: str = "exp"
@registry.envcfg("dm-finger-turn-hard")
@dataclass
class FingerTurnHardCfg(FingerTurnEasyCfg):
model_file: str = turn_hard_model_file
target_radius: float = 0.03
reward_mode: str = "shaped"
turn_reward_shape: str = "exp"

View File

@@ -0,0 +1,76 @@
<mujoco model="finger">
<include file="../../common/visual.xml"/>
<include file="../../common/materials.xml"/>
<asset>
<!-- Match cartpole's skybox + floor look -->
<texture name="skybox" type="skybox" builtin="gradient" rgb1="0.4 0.4 0.4" rgb2="0 0 0" width="512" height="512"/>
<texture name="motphys-ground" type="2d" file="../../common/motphys-ground.png"/>
<material name="motphys-ground" texture="motphys-ground" texuniform="true" texrepeat="0.4 0.4"/>
</asset>
<option timestep="0.01" cone="elliptic" iterations="200">
<flag gravity="disable"/>
</option>
<default>
<geom solimp="0 0.9 0.01" solref=".02 1"/>
<joint type="hinge" axis="0 -1 0"/>
<motor ctrllimited="true" ctrlrange="-1 1"/>
<default class="finger">
<joint damping="2.5" limited="true"/>
<site type="sphere" size=".03" material="site" group="3"/>
</default>
</default>
<worldbody>
<light name="light" directional="true" diffuse=".6 .6 .6" pos="0 0 2" specular=".3 .3 .3"/>
<geom name="ground" type="plane" pos="0 0 0" size="0 0 0.01" material="motphys-ground"/>
<camera name="cam0" pos="0 -1 .8" xyaxes="1 0 0 0 1 2"/>
<camera name="cam1" pos="0 -1 .4" xyaxes="1 0 0 0 0 1"/>
<body name="proximal" pos="-.2 0 .4" childclass="finger">
<geom name="proximal_decoration" type="cylinder" fromto="0 -.033 0 0 .033 0" size=".034" material="decoration"/>
<joint name="proximal" range="-110 110" ref="-90"/>
<geom name="proximal" type="capsule" material="self" size=".03" fromto="0 0 0 0 0 -.17"/>
<body name="distal" pos="0 0 -.18" childclass="finger">
<joint name="distal" range="-110 110"/>
<geom name="distal" type="capsule" size=".028" material="self" fromto="0 0 0 0 0 -.16" contype="0" conaffinity="0"/>
<geom name="fingertip" type="capsule" size=".03" material="effector" fromto="0 0 -.13 0 0 -.161"/>
<site name="touchtop" pos=".01 0 -.17"/>
<site name="touchbottom" pos="-.01 0 -.17"/>
</body>
</body>
<body name="spinner" pos=".2 0 .4">
<joint name="hinge" frictionloss=".1" damping=".5"/>
<geom name="cap1" type="capsule" size=".04 .09" material="self" pos=".02 0 0"/>
<geom name="cap2" type="capsule" size=".04 .09" material="self" pos="-.02 0 0"/>
<site name="tip" type="sphere" size=".02" pos="0 0 .13" material="target"/>
<geom name="spinner_decoration" type="cylinder" fromto="0 -.045 0 0 .045 0" size=".02" material="decoration"/>
</body>
<site name="target" type="sphere" size=".03" pos="0 0 .4" material="target"/>
</worldbody>
<actuator>
<motor name="proximal" joint="proximal" gear="30"/>
<motor name="distal" joint="distal" gear="15"/>
</actuator>
<!-- All finger observations are functions of sensors. This is useful for finite-differencing. -->
<sensor>
<jointpos name="proximal" joint="proximal"/>
<jointpos name="distal" joint="distal"/>
<jointvel name="proximal_velocity" joint="proximal"/>
<jointvel name="distal_velocity" joint="distal"/>
<jointvel name="hinge_velocity" joint="hinge"/>
<framepos name="tip" objtype="site" objname="tip"/>
<framepos name="target" objtype="site" objname="target"/>
<framepos name="spinner" objtype="xbody" objname="spinner"/>
<touch name="touchtop" site="touchtop"/>
<touch name="touchbottom" site="touchbottom"/>
<framepos name="touchtop_pos" objtype="site" objname="touchtop"/>
<framepos name="touchbottom_pos" objtype="site" objname="touchbottom"/>
</sensor>
</mujoco>

View File

@@ -0,0 +1,431 @@
# 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.finger.cfg import FingerBaseCfg
from motrix_envs.np.env import NpEnv, NpEnvState
def _sanitize_joint_limits(low: np.ndarray, high: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
low = low.copy()
high = high.copy()
low = np.where(np.isfinite(low), low, -np.pi)
high = np.where(np.isfinite(high), high, np.pi)
return low, high
class FingerEnv(NpEnv):
_cfg: FingerBaseCfg
_observation_space: gym.spaces.Box
_action_space: gym.spaces.Box
def __init__(self, cfg: FingerBaseCfg, num_envs: int = 1):
super().__init__(cfg, num_envs=num_envs)
self._cfg = cfg
self._spinner = self._model.get_link("spinner")
self._tip_site = self._model.get_site("tip")
self._target_site = self._model.get_site("target")
self._cap1 = self._model.get_geom("cap1")
self._touchtop_site = self._model.get_site("touchtop")
self._touchbottom_site = self._model.get_site("touchbottom")
self._joint_limit_low, self._joint_limit_high = _sanitize_joint_limits(*self._model.joint_limits)
# Cache joint dof indices
self._prox_qpos_i = self._joint_pos_index("proximal")
self._dist_qpos_i = self._joint_pos_index("distal")
self._hinge_qpos_i = self._joint_pos_index("hinge")
self._hinge_qvel_i = self._joint_vel_index("hinge")
self._prox_qvel_i = self._joint_vel_index("proximal")
self._dist_qvel_i = self._joint_vel_index("distal")
self._target_xyz = np.zeros((num_envs, 3), dtype=np.float32)
self._target_radius = float(cfg.target_radius)
self._spin_vel_threshold = float(cfg.spin_velocity_threshold)
self._init_obs_space()
self._init_action_space()
def _joint_pos_index(self, joint_name: str) -> int:
joint_index = self._model.get_joint_index(joint_name)
return int(self._model.joint_dof_pos_indices[joint_index])
def _joint_vel_index(self, joint_name: str) -> int:
joint_index = self._model.get_joint_index(joint_name)
return int(self._model.joint_dof_vel_indices[joint_index])
def _init_obs_space(self):
raise NotImplementedError
def _init_action_space(self):
low, high = self._model.actuator_ctrl_limits
self._action_space = gym.spaces.Box(low, high, (self._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: np.ndarray, state: NpEnvState) -> NpEnvState:
# Keep track of actions for reward shaping (e.g., smoothness penalties)
if "actions" not in state.info:
state.info["actions"] = np.zeros_like(actions, dtype=np.float32)
if "last_actions" not in state.info:
state.info["last_actions"] = np.zeros_like(actions, dtype=np.float32)
state.info["last_actions"] = state.info["actions"]
state.info["actions"] = actions
state.data.actuator_ctrls = actions
return state
def _touch(self, data: mtx.SceneData) -> np.ndarray:
top = np.asarray(self._model.get_sensor_value("touchtop", data)).reshape(data.shape[0], -1)[:, 0]
bottom = np.asarray(self._model.get_sensor_value("touchbottom", data)).reshape(data.shape[0], -1)[:, 0]
return np.log1p(np.stack([top, bottom], axis=-1))
def _tip_position_xz(self, data: mtx.SceneData) -> np.ndarray:
tip_xyz = self._tip_site.get_position(data)
spinner_xyz = self._spinner.get_position(data)
return (tip_xyz - spinner_xyz)[:, [0, 2]]
def _target_position_xz(self, data: mtx.SceneData) -> np.ndarray:
spinner_xyz = self._spinner.get_position(data)
return (self._target_xyz - spinner_xyz)[:, [0, 2]]
def _dist_to_target(self, data: mtx.SceneData) -> np.ndarray:
# Signed distance to the target surface. Negative means inside.
tip_xyz = self._tip_site.get_position(data)
dist = np.linalg.norm((self._target_xyz - tip_xyz)[:, [0, 2]], axis=-1)
return dist - self._target_radius
def _get_obs(self, data: mtx.SceneData) -> np.ndarray:
raise NotImplementedError
def update_state(self, state: NpEnvState) -> NpEnvState:
raise NotImplementedError
def _maybe_init_target_freejoint(self, dof_pos: np.ndarray) -> slice | None:
# Optional freejoint-backed target visualization body (7 qpos: xyz + quat).
try:
self._model.get_geom("target_geom")
target_free_pos = slice(self._model.num_dof_pos - 7, self._model.num_dof_pos)
dof_pos[:, target_free_pos] = np.array([0.0, 0.0, 0.4, 0.0, 0.0, 0.0, 1.0], dtype=np.float32)
return target_free_pos
except Exception:
return None
def _reset_collision_free_joint_angles(
self, data: mtx.SceneData, dof_pos: np.ndarray, target_free_pos: slice | None
):
# Randomize joint angles with a collision-free rejection sampler (dm_control-style).
# The MotrixSim joint_limits are per-joint (not per-DOF), so we explicitly fill each DOF.
num = int(data.shape[0])
max_attempts = int(getattr(self._cfg, "reset_collision_free_attempts", 200))
pending = np.ones((num,), dtype=bool)
for _ in range(max_attempts):
if not pending.any():
break
num_pending = int(pending.sum())
for joint_name in ("proximal", "distal"):
j = self._model.get_joint_index(joint_name)
dof_i = self._joint_pos_index(joint_name)
low = float(self._joint_limit_low[j])
high = float(self._joint_limit_high[j])
dof_pos[pending, dof_i] = np.random.uniform(low=low, high=high, size=(num_pending,)).astype(np.float32)
# Sample hinge position (unlimited in model)
dof_pos[pending, self._hinge_qpos_i] = np.random.uniform(
low=-np.pi, high=np.pi, size=(num_pending,)
).astype(np.float32)
if target_free_pos is not None:
dof_pos[:, target_free_pos] = np.array([0.0, 0.0, 0.4, 0.0, 0.0, 0.0, 1.0], dtype=np.float32)
data.set_dof_pos(dof_pos, self._model)
data.set_dof_vel(np.zeros((num, self._model.num_dof_vel), dtype=np.float32))
self._model.forward_kinematic(data)
pending = self._model.get_contact_query(data).num_contacts > 0
def reset(self, data: mtx.SceneData) -> tuple[np.ndarray, dict]:
raise NotImplementedError
@registry.env("dm-finger-spin", "np")
class FingerSpinEnv(FingerEnv):
def _init_obs_space(self):
# Match dm_control's observation dict, but flatten into a vector.
# Spin: position(4) + velocity(3) + touch(2) = 9
self._observation_space = gym.spaces.Box(-np.inf, np.inf, (9,), dtype=np.float32)
def _get_obs(self, data: mtx.SceneData) -> np.ndarray:
qpos = data.dof_pos
qvel = data.dof_vel
position = np.concatenate(
[
qpos[:, [self._prox_qpos_i, self._dist_qpos_i]],
self._tip_position_xz(data),
],
axis=-1,
)
velocity = qvel[:, [self._prox_qvel_i, self._dist_qvel_i, self._hinge_qvel_i]]
touch = self._touch(data)
return np.concatenate([position, velocity, touch], axis=-1).astype(np.float32)
def update_state(self, state: NpEnvState) -> NpEnvState:
data = state.data
obs = self._get_obs(data)
terminated = np.isnan(obs).any(axis=-1)
hinge_velocity = data.dof_vel[:, self._hinge_qvel_i]
spin_sparse = (hinge_velocity <= -self._spin_vel_threshold).astype(np.float32)
if self._cfg.reward_mode == "shaped":
# Dense reward to help PPO learn: encourage fast negative hinge velocity.
# Range: [0, 1] roughly, with 1 around reaching the threshold.
spin = np.clip((-hinge_velocity) / self._spin_vel_threshold, 0.0, 1.0).astype(np.float32)
if self._cfg.shaped_reward_beta != 1.0:
spin = np.power(spin, self._cfg.shaped_reward_beta, dtype=np.float32)
else:
spin = spin_sparse
touch_raw = np.zeros((data.shape[0],), dtype=np.float32)
touch_bonus = np.zeros((data.shape[0],), dtype=np.float32)
approach_dist = np.zeros((data.shape[0],), dtype=np.float32)
approach_reward = np.zeros((data.shape[0],), dtype=np.float32)
if self._cfg.reward_mode == "shaped":
if float(getattr(self._cfg, "spin_touch_bonus_scale", 0.0)) > 0.0:
top = np.asarray(self._model.get_sensor_value("touchtop", data)).reshape(data.shape[0], -1)[:, 0]
bottom = np.asarray(self._model.get_sensor_value("touchbottom", data)).reshape(data.shape[0], -1)[:, 0]
touch_raw = (top + bottom).astype(np.float32)
touch_bonus = (
float(self._cfg.spin_touch_bonus_scale)
* np.tanh(touch_raw / float(max(self._cfg.spin_touch_bonus_tanh_scale, 1e-6)))
).astype(np.float32)
if float(getattr(self._cfg, "spin_approach_reward_scale", 0.0)) > 0.0:
spinner_xyz = self._spinner.get_position(data)
top_xyz = self._touchtop_site.get_position(data)
bottom_xyz = self._touchbottom_site.get_position(data)
top_dist = np.linalg.norm((top_xyz - spinner_xyz)[:, [0, 2]], axis=-1)
bottom_dist = np.linalg.norm((bottom_xyz - spinner_xyz)[:, [0, 2]], axis=-1)
approach_dist = np.minimum(top_dist, bottom_dist).astype(np.float32)
sigma = float(max(self._cfg.spin_approach_sigma, 1e-6))
approach_reward = (float(self._cfg.spin_approach_reward_scale) * np.exp(-approach_dist / sigma)).astype(
np.float32
)
spin = np.clip(spin + touch_bonus + approach_reward, 0.0, 1.0).astype(np.float32)
rwd = spin
state.info["Reward"] = {
"hinge_velocity": hinge_velocity.copy(),
"spin": spin.copy(),
"spin_sparse": spin_sparse.copy(),
"touch_raw": touch_raw.copy(),
"touch_bonus": touch_bonus.copy(),
"approach_dist": approach_dist.copy(),
"approach_reward": approach_reward.copy(),
}
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 = int(data.shape[0])
dof_pos = np.zeros((num, self._model.num_dof_pos), dtype=np.float32)
target_free_pos = self._maybe_init_target_freejoint(dof_pos)
self._reset_collision_free_joint_angles(data, dof_pos, target_free_pos)
info: dict = {"Reward": {}}
info["actions"] = np.zeros((num, self._model.num_actuators), dtype=np.float32)
info["last_actions"] = np.zeros((num, self._model.num_actuators), dtype=np.float32)
info["Reward"] = {
"hinge_velocity": np.zeros((num,), dtype=np.float32),
"spin": np.zeros((num,), dtype=np.float32),
"spin_sparse": np.zeros((num,), dtype=np.float32),
"touch_raw": np.zeros((num,), dtype=np.float32),
"touch_bonus": np.zeros((num,), dtype=np.float32),
"approach_dist": np.zeros((num,), dtype=np.float32),
"approach_reward": np.zeros((num,), dtype=np.float32),
}
obs = self._get_obs(data)
return obs, info
@registry.env("dm-finger-turn-easy", "np")
@registry.env("dm-finger-turn-hard", "np")
class FingerTurnEnv(FingerEnv):
def _init_obs_space(self):
# Match dm_control's observation dict, but flatten into a vector.
# Turn: position(4) + velocity(3) + touch(2) + target_position(2) + dist_to_target(1) = 12
self._observation_space = gym.spaces.Box(-np.inf, np.inf, (12,), dtype=np.float32)
def _get_obs(self, data: mtx.SceneData) -> np.ndarray:
qpos = data.dof_pos
qvel = data.dof_vel
position = np.concatenate(
[
qpos[:, [self._prox_qpos_i, self._dist_qpos_i]],
self._tip_position_xz(data),
],
axis=-1,
)
velocity = qvel[:, [self._prox_qvel_i, self._dist_qvel_i, self._hinge_qvel_i]]
touch = self._touch(data)
target_position = self._target_position_xz(data)
dist_to_target = self._dist_to_target(data).reshape(data.shape[0], 1)
return np.concatenate([position, velocity, touch, target_position, dist_to_target], axis=-1).astype(np.float32)
def update_state(self, state: NpEnvState) -> NpEnvState:
data = state.data
obs = self._get_obs(data)
terminated = np.isnan(obs).any(axis=-1)
dist_to_target = self._dist_to_target(data)
turn_sparse = (dist_to_target <= 0.0).astype(np.float32)
touch_raw = np.zeros((data.shape[0],), dtype=np.float32)
touch_bonus = np.zeros((data.shape[0],), dtype=np.float32)
approach_dist = np.zeros((data.shape[0],), dtype=np.float32)
approach_reward = np.zeros((data.shape[0],), dtype=np.float32)
action_l2 = np.zeros((data.shape[0],), dtype=np.float32)
action_delta_l2 = np.zeros((data.shape[0],), dtype=np.float32)
if self._cfg.reward_mode == "shaped":
# Encourage approaching the spinner so the agent actually makes contact and can rotate it.
spinner_xyz = self._spinner.get_position(data)
top_xyz = self._touchtop_site.get_position(data)
bottom_xyz = self._touchbottom_site.get_position(data)
top_dist = np.linalg.norm((top_xyz - spinner_xyz)[:, [0, 2]], axis=-1)
bottom_dist = np.linalg.norm((bottom_xyz - spinner_xyz)[:, [0, 2]], axis=-1)
approach_dist = np.minimum(top_dist, bottom_dist).astype(np.float32)
sigma = max(float(self._cfg.turn_approach_sigma), 1e-6)
approach_reward = (self._cfg.turn_approach_reward_scale * np.exp(-approach_dist / sigma)).astype(np.float32)
dist_pos = np.maximum(dist_to_target, 0.0).astype(np.float32)
if getattr(self._cfg, "turn_reward_shape", "linear") == "exp":
sigma = float(
max(self._cfg.turn_reward_sigma_scale * self._target_radius, self._cfg.turn_reward_sigma_min)
)
sigma = max(sigma, 1e-6)
turn = np.exp(-dist_pos / sigma).astype(np.float32)
else:
margin = float(
max(self._cfg.turn_reward_margin_scale * self._target_radius, self._cfg.turn_reward_min_margin)
)
margin = max(margin, 1e-6)
# Dense reward: 1 inside target sphere, decays to 0 at `margin` outside.
turn = np.clip(1.0 - dist_pos / margin, 0.0, 1.0).astype(np.float32)
if self._cfg.turn_shaped_reward_beta != 1.0:
turn = np.power(turn, self._cfg.turn_shaped_reward_beta, dtype=np.float32)
# Encourage making contact (to actually be able to rotate the spinner)
top = np.asarray(self._model.get_sensor_value("touchtop", data)).reshape(data.shape[0], -1)[:, 0]
bottom = np.asarray(self._model.get_sensor_value("touchbottom", data)).reshape(data.shape[0], -1)[:, 0]
touch_raw = (top + bottom).astype(np.float32)
touch_bonus = self._cfg.turn_touch_bonus_scale * np.tanh(touch_raw / self._cfg.turn_touch_bonus_tanh_scale)
# Reduce jitter: penalize large actions and action changes
actions = state.info.get("actions", data.actuator_ctrls).astype(np.float32)
last_actions = state.info.get("last_actions", actions).astype(np.float32)
action_l2 = np.mean(np.square(actions), axis=-1).astype(np.float32)
action_delta_l2 = np.mean(np.square(actions - last_actions), axis=-1).astype(np.float32)
turn = (
turn
+ approach_reward
+ touch_bonus
- self._cfg.turn_action_l2_penalty_scale * action_l2
- self._cfg.turn_action_delta_l2_penalty_scale * action_delta_l2
).astype(np.float32)
turn = np.clip(turn, 0.0, 1.0).astype(np.float32)
else:
turn = turn_sparse
rwd = turn
state.info["Reward"] = {
"dist_to_target": dist_to_target.copy(),
"turn": turn.copy(),
"turn_sparse": turn_sparse.copy(),
"touch_raw": touch_raw.copy(),
"touch_bonus": touch_bonus.copy(),
"approach_dist": approach_dist.copy(),
"approach_reward": approach_reward.copy(),
"action_l2": action_l2.copy(),
"action_delta_l2": action_delta_l2.copy(),
}
state.info["target_info"] = {"positions": self._target_xyz.copy(), "radius": self._target_radius}
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 = int(data.shape[0])
dof_pos = np.zeros((num, self._model.num_dof_pos), dtype=np.float32)
target_free_pos = self._maybe_init_target_freejoint(dof_pos)
self._reset_collision_free_joint_angles(data, dof_pos, target_free_pos)
hinge_xyz = self._spinner.get_position(data)
# Match dm_control: radius = cap1.geom_size.sum() for capsule (radius + half-length).
radius = float(np.sum(self._cap1.size[:2]))
target_angle = np.random.uniform(-np.pi, np.pi, size=(num,))
target_x = hinge_xyz[:, 0] + radius * np.sin(target_angle)
target_z = hinge_xyz[:, 2] + radius * np.cos(target_angle)
self._target_xyz = np.stack([target_x, hinge_xyz[:, 1], target_z], axis=-1).astype(np.float32)
# Best-effort visualization when num_envs == 1 (site position is model-shared).
if self._num_envs == 1:
try:
self._target_site.local_pos = self._target_xyz[0]
self._target_site.size = np.asarray([self._target_radius], dtype=np.float32)
except Exception:
pass
# If we have a freejoint-backed visual target (geom), set its pose in the state.
if target_free_pos is not None:
dof_pos[:, target_free_pos] = np.concatenate(
[
self._target_xyz.astype(np.float32),
np.tile(np.array([[0.0, 0.0, 0.0, 1.0]], dtype=np.float32), (num, 1)),
],
axis=-1,
)
data.set_dof_pos(dof_pos, self._model)
self._model.forward_kinematic(data)
info: dict = {"Reward": {}}
info["actions"] = np.zeros((num, self._model.num_actuators), dtype=np.float32)
info["last_actions"] = np.zeros((num, self._model.num_actuators), dtype=np.float32)
info["target_info"] = {"positions": self._target_xyz.copy(), "radius": self._target_radius}
info["Reward"] = {
"dist_to_target": np.zeros((num,), dtype=np.float32),
"turn": np.zeros((num,), dtype=np.float32),
"turn_sparse": np.zeros((num,), dtype=np.float32),
}
obs = self._get_obs(data)
return obs, info

View File

@@ -0,0 +1,77 @@
<mujoco model="finger">
<include file="../../common/visual.xml"/>
<include file="../../common/materials.xml"/>
<asset>
<!-- Match cartpole's skybox + floor look -->
<texture name="skybox" type="skybox" builtin="gradient" rgb1="0.4 0.4 0.4" rgb2="0 0 0" width="512" height="512"/>
<texture name="motphys-ground" type="2d" file="../../common/motphys-ground.png"/>
<material name="motphys-ground" texture="motphys-ground" texuniform="true" texrepeat="0.4 0.4"/>
</asset>
<option timestep="0.01" cone="elliptic" iterations="200">
<flag gravity="disable"/>
</option>
<default>
<geom solimp="0 0.9 0.01" solref=".02 1"/>
<joint type="hinge" axis="0 -1 0"/>
<motor ctrllimited="true" ctrlrange="-1 1"/>
<default class="finger">
<joint damping="2.5" limited="true"/>
<site type="sphere" size=".03" material="site" group="3"/>
</default>
</default>
<worldbody>
<light name="light" directional="true" diffuse=".6 .6 .6" pos="0 0 2" specular=".3 .3 .3"/>
<geom name="ground" type="plane" pos="0 0 0" size="0 0 0.01" material="motphys-ground"/>
<camera name="cam0" pos="0 -1 .8" xyaxes="1 0 0 0 1 2"/>
<camera name="cam1" pos="0 -1 .4" xyaxes="1 0 0 0 0 1"/>
<body name="proximal" pos="-.2 0 .4" childclass="finger">
<geom name="proximal_decoration" type="cylinder" fromto="0 -.033 0 0 .033 0" size=".034" material="decoration"/>
<joint name="proximal" range="-110 110" ref="-90"/>
<geom name="proximal" type="capsule" material="self" size=".03" fromto="0 0 0 0 0 -.17"/>
<body name="distal" pos="0 0 -.18" childclass="finger">
<joint name="distal" range="-110 110"/>
<geom name="distal" type="capsule" size=".028" material="self" fromto="0 0 0 0 0 -.16" contype="0" conaffinity="0"/>
<geom name="fingertip" type="capsule" size=".03" material="effector" fromto="0 0 -.13 0 0 -.161"/>
<site name="touchtop" pos=".01 0 -.17"/>
<site name="touchbottom" pos="-.01 0 -.17"/>
</body>
</body>
<body name="spinner" pos=".2 0 .4">
<!-- dm_control's Spin task reduces hinge damping for easier spinning -->
<joint name="hinge" frictionloss=".1" damping=".03"/>
<geom name="cap1" type="capsule" size=".04 .09" material="self" pos=".02 0 0"/>
<geom name="cap2" type="capsule" size=".04 .09" material="self" pos="-.02 0 0"/>
<site name="tip" type="sphere" size=".02" pos="0 0 .13" material="target"/>
<geom name="spinner_decoration" type="cylinder" fromto="0 -.045 0 0 .045 0" size=".02" material="decoration"/>
</body>
<site name="target" type="sphere" size=".03" pos="0 0 .4" material="target"/>
</worldbody>
<actuator>
<motor name="proximal" joint="proximal" gear="30"/>
<motor name="distal" joint="distal" gear="15"/>
</actuator>
<sensor>
<jointpos name="proximal" joint="proximal"/>
<jointpos name="distal" joint="distal"/>
<jointvel name="proximal_velocity" joint="proximal"/>
<jointvel name="distal_velocity" joint="distal"/>
<jointvel name="hinge_velocity" joint="hinge"/>
<framepos name="tip" objtype="site" objname="tip"/>
<framepos name="target" objtype="site" objname="target"/>
<framepos name="spinner" objtype="xbody" objname="spinner"/>
<touch name="touchtop" site="touchtop"/>
<touch name="touchbottom" site="touchbottom"/>
<framepos name="touchtop_pos" objtype="site" objname="touchtop"/>
<framepos name="touchbottom_pos" objtype="site" objname="touchbottom"/>
</sensor>
</mujoco>

View File

@@ -0,0 +1,83 @@
<mujoco model="finger">
<include file="../../common/visual.xml"/>
<include file="../../common/materials.xml"/>
<asset>
<!-- Match cartpole's skybox + floor look -->
<texture name="skybox" type="skybox" builtin="gradient" rgb1="0.4 0.4 0.4" rgb2="0 0 0" width="512" height="512"/>
<texture name="motphys-ground" type="2d" file="../../common/motphys-ground.png"/>
<material name="motphys-ground" texture="motphys-ground" texuniform="true" texrepeat="0.4 0.4"/>
</asset>
<option timestep="0.01" cone="elliptic" iterations="200">
<flag gravity="disable"/>
</option>
<default>
<geom solimp="0 0.9 0.01" solref=".02 1"/>
<joint type="hinge" axis="0 -1 0"/>
<motor ctrllimited="true" ctrlrange="-1 1"/>
<default class="finger">
<joint damping="2.5" limited="true"/>
<site type="sphere" size=".03" material="site" group="3"/>
</default>
</default>
<worldbody>
<light name="light" directional="true" diffuse=".6 .6 .6" pos="0 0 2" specular=".3 .3 .3"/>
<geom name="ground" type="plane" pos="0 0 0" size="0 0 0.01" material="motphys-ground"/>
<camera name="cam0" pos="0 -1 .8" xyaxes="1 0 0 0 1 2"/>
<camera name="cam1" pos="0 -1 .4" xyaxes="1 0 0 0 0 1"/>
<body name="proximal" pos="-.2 0 .4" childclass="finger">
<geom name="proximal_decoration" type="cylinder" fromto="0 -.033 0 0 .033 0" size=".034" material="decoration"/>
<joint name="proximal" range="-110 110" ref="-90"/>
<geom name="proximal" type="capsule" material="self" size=".03" fromto="0 0 0 0 0 -.17"/>
<body name="distal" pos="0 0 -.18" childclass="finger">
<joint name="distal" range="-110 110"/>
<geom name="distal" type="capsule" size=".028" material="self" fromto="0 0 0 0 0 -.16" contype="0" conaffinity="0"/>
<geom name="fingertip" type="capsule" size=".03" material="effector" fromto="0 0 -.13 0 0 -.161"/>
<site name="touchtop" pos=".01 0 -.17"/>
<site name="touchbottom" pos="-.01 0 -.17"/>
</body>
</body>
<body name="spinner" pos=".2 0 .4">
<joint name="hinge" frictionloss=".1" damping=".5"/>
<geom name="cap1" type="capsule" size=".04 .09" material="self" pos=".02 0 0"/>
<geom name="cap2" type="capsule" size=".04 .09" material="self" pos="-.02 0 0"/>
<site name="tip" type="sphere" size=".02" pos="0 0 .13" material="target"/>
<geom name="spinner_decoration" type="cylinder" fromto="0 -.045 0 0 .045 0" size=".02" material="decoration"/>
</body>
<!-- Target as a Site (used for sensors) -->
<site name="target" type="sphere" size=".03" pos="0 0 .4" material="target"/>
<!-- Visible target geometry (MotrixSim renderer may not draw sites) -->
<body name="target_vis" pos="0 0 .4">
<freejoint name="target_free"/>
<geom name="target_geom" type="sphere" size=".07" material="target" contype="0" conaffinity="0"/>
</body>
</worldbody>
<actuator>
<motor name="proximal" joint="proximal" gear="30"/>
<motor name="distal" joint="distal" gear="15"/>
</actuator>
<!-- All finger observations are functions of sensors. This is useful for finite-differencing. -->
<sensor>
<jointpos name="proximal" joint="proximal"/>
<jointpos name="distal" joint="distal"/>
<jointvel name="proximal_velocity" joint="proximal"/>
<jointvel name="distal_velocity" joint="distal"/>
<jointvel name="hinge_velocity" joint="hinge"/>
<framepos name="tip" objtype="site" objname="tip"/>
<framepos name="target" objtype="site" objname="target"/>
<framepos name="spinner" objtype="xbody" objname="spinner"/>
<touch name="touchtop" site="touchtop"/>
<touch name="touchbottom" site="touchbottom"/>
<framepos name="touchtop_pos" objtype="site" objname="touchtop"/>
<framepos name="touchbottom_pos" objtype="site" objname="touchbottom"/>
</sensor>
</mujoco>

View File

@@ -0,0 +1,82 @@
<mujoco model="finger">
<include file="../../common/visual.xml"/>
<include file="../../common/materials.xml"/>
<asset>
<!-- Match cartpole's skybox + floor look -->
<texture name="skybox" type="skybox" builtin="gradient" rgb1="0.4 0.4 0.4" rgb2="0 0 0" width="512" height="512"/>
<texture name="motphys-ground" type="2d" file="../../common/motphys-ground.png"/>
<material name="motphys-ground" texture="motphys-ground" texuniform="true" texrepeat="0.4 0.4"/>
</asset>
<option timestep="0.01" cone="elliptic" iterations="200">
<flag gravity="disable"/>
</option>
<default>
<geom solimp="0 0.9 0.01" solref=".02 1"/>
<joint type="hinge" axis="0 -1 0"/>
<motor ctrllimited="true" ctrlrange="-1 1"/>
<default class="finger">
<joint damping="2.5" limited="true"/>
<site type="sphere" size=".03" material="site" group="3"/>
</default>
</default>
<worldbody>
<light name="light" directional="true" diffuse=".6 .6 .6" pos="0 0 2" specular=".3 .3 .3"/>
<geom name="ground" type="plane" pos="0 0 0" size="0 0 0.01" material="motphys-ground"/>
<camera name="cam0" pos="0 -1 .8" xyaxes="1 0 0 0 1 2"/>
<camera name="cam1" pos="0 -1 .4" xyaxes="1 0 0 0 0 1"/>
<body name="proximal" pos="-.2 0 .4" childclass="finger">
<geom name="proximal_decoration" type="cylinder" fromto="0 -.033 0 0 .033 0" size=".034" material="decoration"/>
<joint name="proximal" range="-110 110" ref="-90"/>
<geom name="proximal" type="capsule" material="self" size=".03" fromto="0 0 0 0 0 -.17"/>
<body name="distal" pos="0 0 -.18" childclass="finger">
<joint name="distal" range="-110 110"/>
<geom name="distal" type="capsule" size=".028" material="self" fromto="0 0 0 0 0 -.16" contype="0" conaffinity="0"/>
<geom name="fingertip" type="capsule" size=".03" material="effector" fromto="0 0 -.13 0 0 -.161"/>
<site name="touchtop" pos=".01 0 -.17"/>
<site name="touchbottom" pos="-.01 0 -.17"/>
</body>
</body>
<body name="spinner" pos=".2 0 .4">
<joint name="hinge" frictionloss=".1" damping=".5"/>
<geom name="cap1" type="capsule" size=".04 .09" material="self" pos=".02 0 0"/>
<geom name="cap2" type="capsule" size=".04 .09" material="self" pos="-.02 0 0"/>
<site name="tip" type="sphere" size=".02" pos="0 0 .13" material="target"/>
<geom name="spinner_decoration" type="cylinder" fromto="0 -.045 0 0 .045 0" size=".02" material="decoration"/>
</body>
<!-- Target as a Site (used for sensors) -->
<site name="target" type="sphere" size=".03" pos="0 0 .4" material="target"/>
<!-- Visible target geometry (MotrixSim renderer may not draw sites) -->
<body name="target_vis" pos="0 0 .4">
<freejoint name="target_free"/>
<geom name="target_geom" type="sphere" size=".03" material="target" contype="0" conaffinity="0"/>
</body>
</worldbody>
<actuator>
<motor name="proximal" joint="proximal" gear="30"/>
<motor name="distal" joint="distal" gear="15"/>
</actuator>
<sensor>
<jointpos name="proximal" joint="proximal"/>
<jointpos name="distal" joint="distal"/>
<jointvel name="proximal_velocity" joint="proximal"/>
<jointvel name="distal_velocity" joint="distal"/>
<jointvel name="hinge_velocity" joint="hinge"/>
<framepos name="tip" objtype="site" objname="tip"/>
<framepos name="target" objtype="site" objname="target"/>
<framepos name="spinner" objtype="xbody" objname="spinner"/>
<touch name="touchtop" site="touchtop"/>
<touch name="touchbottom" site="touchbottom"/>
<framepos name="touchtop_pos" objtype="site" objname="touchtop"/>
<framepos name="touchbottom_pos" objtype="site" objname="touchbottom"/>
</sensor>
</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 humanoid_np # noqa: F401

View File

@@ -0,0 +1,84 @@
# 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__) + "/humanoid.xml"
@dataclass
class InitStateConfig:
reset_height_factor: float = 0.95
reset_qvel_range: float = 0.01
reset_actuator_range: float = 0.02
hip_yaw_range: tuple[float, float] = (-15.0, 15.0)
hip_roll_range: tuple[float, float] = (-12.0, 12.0)
hip_pitch_range: tuple[float, float] = (-12.0, 12.0)
symmetric_leg_pairs: list[tuple[int, int, tuple[float, float]]] = field(
default_factory=lambda: [
(10, 16, (-18.0, 2.0)),
(11, 17, (-25.0, 20.0)),
(12, 18, (-70.0, 5.0)),
(13, 19, (-45.0, -25.0)),
(14, 20, (-40.0, 0.0)),
(15, 21, (-25.0, 25.0)),
]
)
symmetric_arm_pairs: list[tuple[int, int]] = field(
default_factory=lambda: [
(22, 25),
(23, 26),
(24, 27),
]
)
arm_margin_factor: float = 0.1
@dataclass
class TerminationConfig:
head_height_factor: float = 0.5
torso_upright_threshold: float = 0.2
extreme_vel_threshold: float = 200.0
@registry.envcfg("dm-humanoid-walk")
@dataclass
class HumanoidWalkCfg(EnvCfg):
model_file: str = model_file
max_episode_seconds: float = 25.0
sim_dt: float = 0.01
ctrl_dt: float = 0.01
move_speed: float = 1.0
stand_height: float = 1.4
init_state: InitStateConfig = field(default_factory=InitStateConfig)
termination_config: TerminationConfig = field(default_factory=TerminationConfig)
@registry.envcfg("dm-humanoid-stand")
@dataclass
class HumanoidStandCfg(HumanoidWalkCfg):
move_speed: float = 0.0
@registry.envcfg("dm-humanoid-run")
@dataclass
class HumanoidRunCfg(HumanoidWalkCfg):
move_speed: float = 10.0

View File

@@ -0,0 +1,207 @@
<mujoco model="humanoid">
<include file="../../common/skybox.xml"/>
<include file="../../common/visual.xml"/>
<include file="../../common/materials.xml"/>
<asset>
<texture name="motphys-ground" type="2d" file="../../common/motphys-ground.png" />
<material name="motphys-ground" texture="motphys-ground" texuniform="true"
texrepeat="0.4 0.4" />
</asset>
<statistic extent="2" center="0 0 1"/>
<option timestep=".005"/>
<default>
<motor ctrlrange="-1 1" ctrllimited="true"/>
<default class="body">
<geom type="capsule" condim="1" friction=".7" solimp=".9 .99 .003" solref=".015 1" material="self"/>
<joint type="hinge" damping=".2" stiffness="1" armature=".01" limited="true" solimplimit="0 .99 .01"/>
<default class="big_joint">
<joint damping="5" stiffness="10"/>
<default class="big_stiff_joint">
<joint stiffness="20"/>
</default>
</default>
<site size=".04" group="3"/>
<default class="force-torque">
<site type="box" size=".01 .01 .02" rgba="1 0 0 1" />
</default>
<default class="touch">
<site type="capsule" rgba="0 0 1 .3"/>
</default>
</default>
</default>
<worldbody>
<geom name="floor" size="0 0 0.01" type="plane" material="motphys-ground" contype="1"
conaffinity="1" priority="1" friction="0.6" condim="3" />
<body name="torso" pos="0 0 1.5" childclass="body">
<light name="sun" pos="0 0 8" dir="0 0 -1" directional="true" ambient="0.4 0.4 0.4" diffuse="0.8 0.8 0.8" specular="0.1 0.1 0.1"/>
<camera name="back" pos="-3 0 1" xyaxes="0 -1 0 1 0 2" mode="trackcom"/>
<camera name="side" pos="0 -3 1" xyaxes="1 0 0 0 1 2" mode="trackcom"/>
<freejoint name="root"/>
<site name="root" class="force-torque"/>
<geom name="torso" fromto="0 -.07 0 0 .07 0" size=".07"/>
<geom name="upper_waist" fromto="-.01 -.06 -.12 -.01 .06 -.12" size=".06"/>
<site name="torso" class="touch" type="box" pos="0 0 -.05" size=".075 .14 .13"/>
<body name="head" pos="0 0 .19">
<geom name="head" type="sphere" size=".09"/>
<site name="head" class="touch" type="sphere" size=".091"/>
<camera name="egocentric" pos=".09 0 0" xyaxes="0 -1 0 .1 0 1" fovy="80"/>
</body>
<body name="lower_waist" pos="-.01 0 -.260" quat="1.000 0 -.002 0">
<geom name="lower_waist" fromto="0 -.06 0 0 .06 0" size=".06"/>
<site name="lower_waist" class="touch" size=".061 .06" zaxis="0 1 0"/>
<joint name="abdomen_z" pos="0 0 .065" axis="0 0 1" range="-45 45" class="big_stiff_joint"/>
<joint name="abdomen_y" pos="0 0 .065" axis="0 1 0" range="-75 30" class="big_joint"/>
<body name="pelvis" pos="0 0 -.165" quat="1.000 0 -.002 0">
<joint name="abdomen_x" pos="0 0 .1" axis="1 0 0" range="-35 35" class="big_joint"/>
<geom name="butt" fromto="-.02 -.07 0 -.02 .07 0" size=".09"/>
<site name="butt" class="touch" size=".091 .07" pos="-.02 0 0" zaxis="0 1 0"/>
<body name="right_thigh" pos="0 -.1 -.04">
<site name="right_hip" class="force-torque"/>
<joint name="right_hip_x" axis="1 0 0" range="-25 5" class="big_joint"/>
<joint name="right_hip_z" axis="0 0 1" range="-60 35" class="big_joint"/>
<joint name="right_hip_y" axis="0 1 0" range="-120 20" class="big_stiff_joint"/>
<geom name="right_thigh" fromto="0 0 0 0 .01 -.34" size=".06"/>
<site name="right_thigh" class="touch" pos="0 .005 -.17" size=".061 .17" zaxis="0 -1 34"/>
<body name="right_shin" pos="0 .01 -.403">
<site name="right_knee" class="force-torque" pos="0 0 .02"/>
<joint name="right_knee" pos="0 0 .02" axis="0 -1 0" range="-160 2"/>
<geom name="right_shin" fromto="0 0 0 0 0 -.3" size=".049"/>
<site name="right_shin" class="touch" pos="0 0 -.15" size=".05 .15"/>
<body name="right_foot" pos="0 0 -.39">
<site name="right_ankle" class="force-torque"/>
<joint name="right_ankle_y" pos="0 0 .08" axis="0 1 0" range="-50 50" stiffness="6"/>
<joint name="right_ankle_x" pos="0 0 .04" axis="1 0 .5" range="-50 50" stiffness="3"/>
<geom name="right_right_foot" fromto="-.07 -.02 0 .14 -.04 0" size=".027"/>
<geom name="left_right_foot" fromto="-.07 0 0 .14 .02 0" size=".027"/>
<site name="right_right_foot" class="touch" pos=".035 -.03 0" size=".03 .11" zaxis="21 -2 0"/>
<site name="left_right_foot" class="touch" pos=".035 .01 0" size=".03 .11" zaxis="21 2 0"/>
</body>
</body>
</body>
<body name="left_thigh" pos="0 .1 -.04">
<site name="left_hip" class="force-torque"/>
<joint name="left_hip_x" axis="-1 0 0" range="-25 5" class="big_joint"/>
<joint name="left_hip_z" axis="0 0 -1" range="-60 35" class="big_joint"/>
<joint name="left_hip_y" axis="0 1 0" range="-120 20" class="big_stiff_joint"/>
<geom name="left_thigh" fromto="0 0 0 0 -.01 -.34" size=".06"/>
<site name="left_thigh" class="touch" pos="0 -.005 -.17" size=".061 .17" zaxis="0 1 34"/>
<body name="left_shin" pos="0 -.01 -.403">
<site name="left_knee" class="force-torque" pos="0 0 .02"/>
<joint name="left_knee" pos="0 0 .02" axis="0 -1 0" range="-160 2"/>
<geom name="left_shin" fromto="0 0 0 0 0 -.3" size=".049"/>
<site name="left_shin" class="touch" pos="0 0 -.15" size=".05 .15"/>
<body name="left_foot" pos="0 0 -.39">
<site name="left_ankle" class="force-torque"/>
<joint name="left_ankle_y" pos="0 0 .08" axis="0 1 0" range="-50 50" stiffness="6"/>
<joint name="left_ankle_x" pos="0 0 .04" axis="1 0 .5" range="-50 50" stiffness="3"/>
<geom name="left_left_foot" fromto="-.07 .02 0 .14 .04 0" size=".027"/>
<geom name="right_left_foot" fromto="-.07 0 0 .14 -.02 0" size=".027"/>
<site name="right_left_foot" class="touch" pos=".035 -.01 0" size=".03 .11" zaxis="21 -2 0"/>
<site name="left_left_foot" class="touch" pos=".035 .03 0" size=".03 .11" zaxis="21 2 0"/>
</body>
</body>
</body>
</body>
</body>
<body name="right_upper_arm" pos="0 -.17 .06">
<joint name="right_shoulder1" axis="2 1 1" range="-85 60"/>
<joint name="right_shoulder2" axis="0 -1 1" range="-85 60"/>
<geom name="right_upper_arm" fromto="0 0 0 .16 -.16 -.16" size=".04 .16"/>
<site name="right_upper_arm" class="touch" pos=".08 -.08 -.08" size=".041 .14" zaxis="1 -1 -1"/>
<body name="right_lower_arm" pos=".18 -.18 -.18">
<joint name="right_elbow" axis="0 -1 1" range="-90 50" stiffness="0"/>
<geom name="right_lower_arm" fromto=".01 .01 .01 .17 .17 .17" size=".031"/>
<site name="right_lower_arm" class="touch" pos=".09 .09 .09" size=".032 .14" zaxis="1 1 1"/>
<body name="right_hand" pos=".18 .18 .18">
<geom name="right_hand" type="sphere" size=".04"/>
<site name="right_hand" class="touch" type="sphere" size=".041"/>
</body>
</body>
</body>
<body name="left_upper_arm" pos="0 .17 .06">
<joint name="left_shoulder1" axis="2 -1 1" range="-60 85"/>
<joint name="left_shoulder2" axis="0 1 1" range="-60 85"/>
<geom name="left_upper_arm" fromto="0 0 0 .16 .16 -.16" size=".04 .16"/>
<site name="left_upper_arm" class="touch" pos=".08 .08 -.08" size=".041 .14" zaxis="1 1 -1"/>
<body name="left_lower_arm" pos=".18 .18 -.18">
<joint name="left_elbow" axis="0 -1 -1" range="-90 50" stiffness="0"/>
<geom name="left_lower_arm" fromto=".01 -.01 .01 .17 -.17 .17" size=".031"/>
<site name="left_lower_arm" class="touch" pos=".09 -.09 .09" size=".032 .14" zaxis="1 -1 1"/>
<body name="left_hand" pos=".18 -.18 .18">
<geom name="left_hand" type="sphere" size=".04"/>
<site name="left_hand" class="touch" type="sphere" size=".041"/>
</body>
</body>
</body>
</body>
</worldbody>
<actuator>
<motor name="abdomen_y" gear="40" joint="abdomen_y"/>
<motor name="abdomen_z" gear="40" joint="abdomen_z"/>
<motor name="abdomen_x" gear="40" joint="abdomen_x"/>
<motor name="right_hip_x" gear="40" joint="right_hip_x"/>
<motor name="right_hip_z" gear="40" joint="right_hip_z"/>
<motor name="right_hip_y" gear="120" joint="right_hip_y"/>
<motor name="right_knee" gear="80" joint="right_knee"/>
<motor name="right_ankle_x" gear="20" joint="right_ankle_x"/>
<motor name="right_ankle_y" gear="20" joint="right_ankle_y"/>
<motor name="left_hip_x" gear="40" joint="left_hip_x"/>
<motor name="left_hip_z" gear="40" joint="left_hip_z"/>
<motor name="left_hip_y" gear="120" joint="left_hip_y"/>
<motor name="left_knee" gear="80" joint="left_knee"/>
<motor name="left_ankle_x" gear="20" joint="left_ankle_x"/>
<motor name="left_ankle_y" gear="20" joint="left_ankle_y"/>
<motor name="right_shoulder1" gear="20" joint="right_shoulder1"/>
<motor name="right_shoulder2" gear="20" joint="right_shoulder2"/>
<motor name="right_elbow" gear="40" joint="right_elbow"/>
<motor name="left_shoulder1" gear="20" joint="left_shoulder1"/>
<motor name="left_shoulder2" gear="20" joint="left_shoulder2"/>
<motor name="left_elbow" gear="40" joint="left_elbow"/>
</actuator>
<sensor>
<subtreelinvel name="torso_subtreelinvel" body="torso"/>
<accelerometer name="torso_accel" site="root"/>
<velocimeter name="torso_vel" site="root"/>
<gyro name="torso_gyro" site="root"/>
<force name="left_ankle_force" site="left_ankle"/>
<force name="right_ankle_force" site="right_ankle"/>
<force name="left_knee_force" site="left_knee"/>
<force name="right_knee_force" site="right_knee"/>
<force name="left_hip_force" site="left_hip"/>
<force name="right_hip_force" site="right_hip"/>
<torque name="left_ankle_torque" site="left_ankle"/>
<torque name="right_ankle_torque" site="right_ankle"/>
<torque name="left_knee_torque" site="left_knee"/>
<torque name="right_knee_torque" site="right_knee"/>
<torque name="left_hip_torque" site="left_hip"/>
<torque name="right_hip_torque" site="right_hip"/>
<touch name="torso_touch" site="torso"/>
<touch name="head_touch" site="head"/>
<touch name="lower_waist_touch" site="lower_waist"/>
<touch name="butt_touch" site="butt"/>
<touch name="right_thigh_touch" site="right_thigh"/>
<touch name="right_shin_touch" site="right_shin"/>
<touch name="right_right_foot_touch" site="right_right_foot"/>
<touch name="left_right_foot_touch" site="left_right_foot"/>
<touch name="left_thigh_touch" site="left_thigh"/>
<touch name="left_shin_touch" site="left_shin"/>
<touch name="right_left_foot_touch" site="right_left_foot"/>
<touch name="left_left_foot_touch" site="left_left_foot"/>
<touch name="right_upper_arm_touch" site="right_upper_arm"/>
<touch name="right_lower_arm_touch" site="right_lower_arm"/>
<touch name="right_hand_touch" site="right_hand"/>
<touch name="left_upper_arm_touch" site="left_upper_arm"/>
<touch name="left_lower_arm_touch" site="left_lower_arm"/>
<touch name="left_hand_touch" site="left_hand"/>
</sensor>
</mujoco>

View File

@@ -0,0 +1,550 @@
# 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.humanoid.cfg import HumanoidWalkCfg
from motrix_envs.np import reward
from motrix_envs.np.env import NpEnv, NpEnvState
@registry.env("dm-humanoid-stand", "np")
@registry.env("dm-humanoid-walk", "np")
@registry.env("dm-humanoid-run", "np")
class Humanoid3DEnv(NpEnv):
_observation_space: gym.spaces.Box
_action_space: gym.spaces.Box
def __init__(self, cfg: HumanoidWalkCfg, num_envs=1):
super().__init__(cfg, num_envs)
self._init_obs_space()
self._init_action_space()
self._torso = self._model.get_link("torso")
self._head = self._model.get_link("head")
self._pelvis = self._model.get_link("pelvis")
self._left_hand = self._model.get_link("left_hand")
self._right_hand = self._model.get_link("right_hand")
self._left_foot = self._model.get_link("left_foot")
self._right_foot = self._model.get_link("right_foot")
self._move_speed = float(cfg.move_speed)
self._stand_height = float(cfg.stand_height)
self._target_direction = np.array([1.0, 0.0, 0.0], dtype=np.float32)
self._target_direction_xy = self._target_direction[:2].copy()
self._qpos_low, self._qpos_high = self._build_qpos_limits(self._model)
self._cache_derived_constants(cfg)
self._init_joint_randomization_config(cfg)
def _build_qpos_limits(self, model) -> tuple[np.ndarray, np.ndarray]:
num_dof_pos = int(model.num_dof_pos)
jl = np.asarray(model.joint_limits, dtype=np.float32)
if jl.ndim != 2 or jl.shape[0] != 2:
low = np.full((num_dof_pos,), -np.inf, dtype=np.float32)
high = np.full((num_dof_pos,), np.inf, dtype=np.float32)
return low, high
k = int(jl.shape[1])
low = np.full((num_dof_pos,), -np.inf, dtype=np.float32)
high = np.full((num_dof_pos,), np.inf, dtype=np.float32)
m = min(k, num_dof_pos)
low[:m] = jl[0, :m]
high[:m] = jl[1, :m]
return low, high
def _cache_derived_constants(self, cfg: HumanoidWalkCfg) -> None:
t_cfg = cfg.termination_config
self._head_height_min = self._stand_height * 0.95
self._pelvis_height_min = 0.6 * self._stand_height
self._pelvis_height_margin = 0.6 * self._stand_height
self._term_head_height_min = float(t_cfg.head_height_factor) * self._stand_height
self._term_torso_upright_threshold = float(t_cfg.torso_upright_threshold)
self._term_extreme_vel_threshold = float(t_cfg.extreme_vel_threshold)
def _init_obs_space(self):
model = self._model
num_joint_angles = model.num_dof_pos - 7
num_head_height = 1
num_extremities = 12
num_torso_vertical = 3
num_com_vel = 3
num_qvel = model.num_dof_vel
num_target_local = 3
num_obs = (
num_joint_angles
+ num_head_height
+ num_extremities
+ num_torso_vertical
+ num_com_vel
+ num_qvel
+ num_target_local
)
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(
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: np.ndarray, state: NpEnvState) -> NpEnvState:
state.data.actuator_ctrls = actions
return state
def update_state(self, state: NpEnvState) -> NpEnvState:
state = self.update_observation(state)
state = self.update_terminated(state)
state = self.update_reward(state)
return state
def update_observation(self, state: NpEnvState) -> NpEnvState:
data = state.data
obs = self._get_obs(data)
return state.replace(obs=obs)
def update_terminated(self, state: NpEnvState) -> NpEnvState:
data = state.data
head_height = self._get_head_height(data)
torso_upright = self._get_torso_upright(data)
terminated = self._compute_terminated(data, head_height, torso_upright)
return state.replace(
terminated=terminated,
)
def update_reward(self, state: NpEnvState) -> NpEnvState:
data = state.data
terminated = state.terminated
head_height = self._get_head_height(data)
pelvis_height = self._get_pelvis_height(data)
torso_upright = self._get_torso_upright(data)
rwd, reward_components = self._compute_reward(data, head_height, torso_upright, pelvis_height)
rwd, reward_components = self._apply_termination_mask(terminated, rwd, reward_components)
state.info["Reward"] = reward_components
return state.replace(reward=rwd)
def _apply_termination_mask(
self,
terminated: np.ndarray,
rwd: np.ndarray,
reward_components: dict,
) -> tuple[np.ndarray, dict]:
rwd = np.where(terminated, 0.0, rwd).astype(np.float32)
for k, v in reward_components.items():
reward_components[k] = np.where(terminated, 0.0, v).astype(np.float32)
return rwd, reward_components
def reset(self, data: mtx.SceneData) -> tuple[np.ndarray, dict]:
data.reset(self._model)
self._randomize_joints_inplace(data)
obs = self._get_obs(data)
return obs, {}
def _get_obs(self, data: mtx.SceneData) -> np.ndarray:
joint_angles = np.asarray(data.dof_pos[:, 7:], dtype=np.float32)
head_height = self._get_head_height(data).astype(np.float32)[:, None]
extremities = self._get_extremities(data).astype(np.float32)
torso_rot = self._torso.get_rotation_mat(data)
torso_vertical = np.asarray(torso_rot[:, 2, :], dtype=np.float32)
com_vel = np.asarray(self._model.get_sensor_value("torso_subtreelinvel", data), dtype=np.float32)
qvel = np.asarray(data.dof_vel, dtype=np.float32)
target_direction_local = self._get_target_direction_local(data).astype(np.float32)
obs = np.concatenate(
[joint_angles, head_height, extremities, torso_vertical, com_vel, qvel, target_direction_local], axis=-1
)
return obs
def _get_head_height(self, data: mtx.SceneData) -> np.ndarray:
return np.asarray(self._head.get_position(data)[:, 2], dtype=np.float32)
def _get_pelvis_height(self, data: mtx.SceneData) -> np.ndarray:
return np.asarray(self._pelvis.get_position(data)[:, 2], dtype=np.float32)
def _get_torso_upright(self, data: mtx.SceneData) -> np.ndarray:
torso_rot = self._torso.get_rotation_mat(data)
return np.asarray(torso_rot[:, 2, 2], dtype=np.float32)
def _get_extremities(self, data: mtx.SceneData) -> np.ndarray:
torso_rot = self._torso.get_rotation_mat(data)
torso_pos = self._torso.get_position(data)
parts = [
self._left_hand.get_position(data),
self._left_foot.get_position(data),
self._right_hand.get_position(data),
self._right_foot.get_position(data),
]
out = []
torso_rot_f32 = np.asarray(torso_rot, dtype=np.float32)
torso_pos_f32 = np.asarray(torso_pos, dtype=np.float32)
for p in parts:
torso_to_limb = np.asarray(p, dtype=np.float32) - torso_pos_f32
v_body = np.einsum("ni,nij->nj", torso_to_limb, torso_rot_f32)
out.append(v_body)
return np.concatenate(out, axis=-1)
def _get_target_direction_local(self, data: mtx.SceneData) -> np.ndarray:
n = int(data.shape[0])
torso_rot = self._torso.get_rotation_mat(data)
torso_rot_f32 = np.asarray(torso_rot, dtype=np.float32)
target_world = np.ones((n, 3), dtype=np.float32) * self._target_direction[None, :]
target_local = np.einsum("ni,nij->nj", target_world, torso_rot_f32)
return target_local
def _compute_reward(
self,
data: mtx.SceneData,
head_height: np.ndarray,
torso_upright: np.ndarray,
pelvis_height: np.ndarray,
) -> tuple[np.ndarray, dict]:
posture_reward = self._compute_posture_reward(head_height, torso_upright, pelvis_height)
speed_reward, energy_reward = self._compute_speed_and_energy_reward(data)
gait_reward = self._compute_gait_reward(data)
rwd = (posture_reward * speed_reward * energy_reward * gait_reward).astype(np.float32)
comps = {
"energy": energy_reward.astype(np.float32),
"speed": speed_reward.astype(np.float32),
"posture": posture_reward.astype(np.float32),
"gait": gait_reward.astype(np.float32),
}
return rwd, comps
def _compute_posture_reward(
self,
head_height: np.ndarray,
torso_upright: np.ndarray,
pelvis_height: np.ndarray,
) -> np.ndarray:
stand_reward = (
reward.tolerance(
head_height,
bounds=(self._head_height_min, float("inf")),
margin=0.5,
)
.astype(np.float32)
.flatten()
)
upright_reward = (
reward.tolerance(
torso_upright,
bounds=(0.9, float("inf")),
sigmoid="linear",
margin=0.9,
)
.astype(np.float32)
.flatten()
)
pelvis_height_reward = (
reward.tolerance(
pelvis_height,
bounds=(self._pelvis_height_min, float("inf")),
sigmoid="linear",
margin=self._pelvis_height_margin,
)
.astype(np.float32)
.flatten()
)
return (stand_reward * upright_reward * pelvis_height_reward).astype(np.float32)
def _compute_speed_and_energy_reward(
self,
data: mtx.SceneData,
) -> tuple[np.ndarray, np.ndarray]:
target_dir_xy = self._target_direction_xy
ctrls = np.asarray(data.actuator_ctrls, dtype=np.float32)
com_vel = np.asarray(self._model.get_sensor_value("torso_subtreelinvel", data), dtype=np.float32)
if self._move_speed <= 0.0:
energy_reward = np.exp(-1.0 * np.mean(np.square(ctrls), axis=-1)).astype(np.float32)
actual_speed = np.linalg.norm(com_vel[:, :2], axis=-1).astype(np.float32)
speed_reward = (
reward.tolerance(
actual_speed,
bounds=(self._move_speed, self._move_speed),
margin=1.0,
value_at_margin=0.01,
)
.astype(np.float32)
.flatten()
)
elif self._move_speed <= 3.0:
energy_reward = np.exp(-0.5 * np.mean(np.square(ctrls), axis=-1)).astype(np.float32)
actual_speed = np.sum(com_vel[:, :2] * target_dir_xy, axis=-1).astype(np.float32)
speed_reward = (
reward.tolerance(
actual_speed,
bounds=(self._move_speed, self._move_speed),
margin=self._move_speed,
value_at_margin=0.0,
sigmoid="linear",
)
.astype(np.float32)
.flatten()
)
else:
energy_reward = np.exp(-0.3 * np.mean(np.square(ctrls), axis=-1)).astype(np.float32)
actual_speed = np.sum(com_vel[:, :2] * target_dir_xy, axis=-1).astype(np.float32)
speed_reward = (
reward.tolerance(
actual_speed,
bounds=(self._move_speed, float("inf")),
margin=self._move_speed,
value_at_margin=0.0,
sigmoid="linear",
)
.astype(np.float32)
.flatten()
)
return speed_reward, energy_reward
def _compute_heading_reward(
self,
forward_vec: np.ndarray,
target_dir: np.ndarray,
bounds,
margin,
) -> np.ndarray:
dot = np.sum(forward_vec * target_dir, axis=-1)
return (
reward.tolerance(
dot,
bounds=bounds,
margin=margin,
value_at_margin=0.0,
sigmoid="linear",
)
.astype(np.float32)
.flatten()
)
def _compute_gait_reward(self, data: mtx.SceneData) -> np.ndarray:
target_dir = self._target_direction
torso_rot = self._torso.get_rotation_mat(data)
head_rot = self._head.get_rotation_mat(data)
pelvis_rot = self._pelvis.get_rotation_mat(data)
torso_forward = np.asarray(torso_rot[:, 0, 0:3], dtype=np.float32)
torso_heading_reward = self._compute_heading_reward(torso_forward, target_dir, bounds=(0.9, 1.0), margin=0.3)
head_forward = np.asarray(head_rot[:, 0, 0:3], dtype=np.float32)
head_heading_reward = self._compute_heading_reward(head_forward, target_dir, bounds=(0.9, 1.0), margin=0.3)
pelvis_forward = np.asarray(pelvis_rot[:, 0, 0:3], dtype=np.float32)
pelvis_yaw_reward = self._compute_heading_reward(pelvis_forward, target_dir, bounds=(0.9, 1.0), margin=0.3)
pelvis_up = np.asarray(pelvis_rot[:, 2, 2], dtype=np.float32)
pelvis_level_reward = (
reward.tolerance(
pelvis_up,
bounds=(0.9, 1.0),
margin=0.3,
sigmoid="linear",
value_at_margin=0.0,
)
.astype(np.float32)
.flatten()
)
left_foot_pos = self._left_foot.get_position(data)
right_foot_pos = self._right_foot.get_position(data)
max_foot_h = np.maximum(
np.asarray(left_foot_pos[:, 2], dtype=np.float32),
np.asarray(right_foot_pos[:, 2], dtype=np.float32),
)
feet_height_reward = (
reward.tolerance(
max_foot_h,
bounds=(0.0, 0.3),
margin=0.5,
sigmoid="quadratic",
value_at_margin=0.0,
)
.astype(np.float32)
.flatten()
)
return (
torso_heading_reward * head_heading_reward * pelvis_yaw_reward * pelvis_level_reward * feet_height_reward
).astype(np.float32)
def _compute_terminated(
self,
data: mtx.SceneData,
head_height: np.ndarray,
torso_upright: np.ndarray,
) -> np.ndarray:
qpos = np.asarray(data.dof_pos, dtype=np.float32)
qvel = np.asarray(data.dof_vel, dtype=np.float32)
bad = ~np.isfinite(qpos).all(axis=-1) | ~np.isfinite(qvel).all(axis=-1)
too_low = head_height < self._term_head_height_min
too_tilted = torso_upright < self._term_torso_upright_threshold
extreme_vel = np.abs(qvel).max(axis=-1) > self._term_extreme_vel_threshold
return (bad | too_low | too_tilted | extreme_vel).astype(bool)
def _init_joint_randomization_config(self, cfg: HumanoidWalkCfg) -> None:
init_cfg = cfg.init_state
self._reset_height = self._stand_height * init_cfg.reset_height_factor
self._reset_qvel_range = init_cfg.reset_qvel_range
self._reset_actuator_range = init_cfg.reset_actuator_range
self._hip_yaw_range = tuple(np.deg2rad(x) for x in init_cfg.hip_yaw_range)
self._hip_roll_range = tuple(np.deg2rad(x) for x in init_cfg.hip_roll_range)
self._hip_pitch_range = tuple(np.deg2rad(x) for x in init_cfg.hip_pitch_range)
self._symmetric_leg_pairs_rad = [
(left_idx, right_idx, tuple(np.deg2rad(x) for x in deg_range))
for left_idx, right_idx, deg_range in init_cfg.symmetric_leg_pairs
]
self._symmetric_arm_pairs = init_cfg.symmetric_arm_pairs
self._arm_margin_factor = init_cfg.arm_margin_factor
self._symmetric_arm_used_indices = set()
for left_idx, right_idx in self._symmetric_arm_pairs:
self._symmetric_arm_used_indices.add(left_idx)
self._symmetric_arm_used_indices.add(right_idx)
def _randomize_joints_inplace(self, data: mtx.SceneData) -> None:
# qpos layout (humanoid.xml): 0-6 free (x,y,z,qw,qx,qy,qz), 7=abdomen_z, 8=abdomen_y, 9=abdomen_x,
# 10-15 right leg (hip_x,z,y, knee, ankle_y,x), 16-21 left leg, 22-24 right arm, 25-27 left arm (num_dof_pos=28)
model = self._model
n = int(data.shape[0])
num_dof_pos = int(model.num_dof_pos)
num_dof_vel = int(model.num_dof_vel)
num_actuators = int(model.num_actuators)
low, high = self._qpos_low, self._qpos_high
qpos = np.zeros((n, num_dof_pos), dtype=np.float32)
qpos[:, 2] = self._reset_height
qpos[:, 3] = 1.0
# qpos 7=abdomen_z (yaw), 8=abdomen_y (pitch), 9=abdomen_x (roll) per humanoid.xml
qpos[:, 7] = np.random.uniform(self._hip_yaw_range[0], self._hip_yaw_range[1], size=(n,))
qpos[:, 8] = np.random.uniform(self._hip_pitch_range[0], self._hip_pitch_range[1], size=(n,))
qpos[:, 9] = np.random.uniform(self._hip_roll_range[0], self._hip_roll_range[1], size=(n,))
self._randomize_symmetric_legs(qpos, n, num_dof_pos, low, high)
self._randomize_symmetric_arms(qpos, n, num_dof_pos, low, high)
self._randomize_remaining_joints(qpos, n, num_dof_pos, low, high)
qvel = np.random.uniform(-self._reset_qvel_range, self._reset_qvel_range, size=(n, num_dof_vel)).astype(
np.float32
)
actuator_ctrls = np.random.uniform(
-self._reset_actuator_range, self._reset_actuator_range, size=(n, num_actuators)
).astype(np.float32)
qpos_set = qpos.copy()
qpos_set[:, 3:7] = np.concatenate([qpos[:, 4:7], qpos[:, 3:4]], axis=1)
data.set_dof_pos(qpos_set, self._model)
data.set_dof_vel(qvel)
data.actuator_ctrls[:] = actuator_ctrls
self._model.forward_kinematic(data)
def _randomize_symmetric_legs(
self, qpos: np.ndarray, n: int, num_dof_pos: int, low: np.ndarray, high: np.ndarray
) -> None:
for left_idx, right_idx, (min_rad, max_rad) in self._symmetric_leg_pairs_rad:
if left_idx < num_dof_pos:
qpos[:, left_idx] = np.random.uniform(
np.clip(min_rad, low[left_idx], high[left_idx]),
np.clip(max_rad, low[left_idx], high[left_idx]),
size=(n,),
)
if right_idx < num_dof_pos:
right_min_rad = -max_rad
right_max_rad = -min_rad
qpos[:, right_idx] = np.random.uniform(
np.clip(right_min_rad, low[right_idx], high[right_idx]),
np.clip(right_max_rad, low[right_idx], high[right_idx]),
size=(n,),
)
def _randomize_symmetric_arms(
self, qpos: np.ndarray, n: int, num_dof_pos: int, low: np.ndarray, high: np.ndarray
) -> None:
# Default range when model joint_limits are missing (low/high are ±inf);
# np.random.uniform requires finite bounds.
default_lo, default_hi = -np.pi, np.pi
for left_idx, right_idx in self._symmetric_arm_pairs:
if left_idx < num_dof_pos and right_idx < num_dof_pos:
lo_l = low[left_idx] if np.isfinite(low[left_idx]) else default_lo
hi_l = high[left_idx] if np.isfinite(high[left_idx]) else default_hi
lo_r = low[right_idx] if np.isfinite(low[right_idx]) else default_lo
hi_r = high[right_idx] if np.isfinite(high[right_idx]) else default_hi
left_range = hi_l - lo_l
left_margin = left_range * self._arm_margin_factor
left_min = lo_l + left_margin
left_max = hi_l - left_margin
right_min = -left_max
right_max = -left_min
right_min_clipped = max(right_min, lo_r)
right_max_clipped = min(right_max, hi_r)
if left_min < left_max:
qpos[:, left_idx] = np.random.uniform(left_min, left_max, size=(n,))
else:
qpos[:, left_idx] = np.random.uniform(lo_l, hi_l, size=(n,))
if right_min_clipped < right_max_clipped:
qpos[:, right_idx] = np.random.uniform(right_min_clipped, right_max_clipped, size=(n,))
else:
qpos[:, right_idx] = np.random.uniform(lo_r, hi_r, size=(n,))
def _randomize_remaining_joints(
self, qpos: np.ndarray, n: int, num_dof_pos: int, low: np.ndarray, high: np.ndarray
) -> None:
used_indices = self._symmetric_arm_used_indices
default_lo, default_hi = -np.pi, np.pi
# 22 = first arm joint (right_shoulder1) in humanoid.xml qpos order;
# arms 22-27 are covered by symmetric_arm_pairs
for i in range(22, num_dof_pos):
if i not in used_indices:
lo = low[i] if np.isfinite(low[i]) else default_lo
hi = high[i] if np.isfinite(high[i]) else default_hi
qpos[:, i] = np.random.uniform(lo, hi, size=(n,))

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 manipulator_np # noqa: F401

View File

@@ -0,0 +1,95 @@
# 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
bring_ball_model_file = os.path.join(os.path.dirname(__file__), "manipulator_bring_ball.xml")
@registry.envcfg("dm-manipulator-bring-ball")
@dataclass
class BringBallCfg(EnvCfg):
# Simulation
model_file: str = bring_ball_model_file
max_episode_seconds: float = 10.0
sim_dt: float = 0.001
ctrl_dt: float = 0.01
render_spacing: float = 2.5
# Reset sampling (match dm_control defaults).
p_in_hand: float = 0.1
p_in_target: float = 0.1
randomize_arm: bool = True
# Target sampling.
target_x_range: tuple[float, float] = (-0.4, 0.4)
target_z_range: tuple[float, float] = (0.1, 0.4)
target_y: float = 0.001
target_angle_range: tuple[float, float] = (-3.14159265, 3.14159265)
# Object sampling.
object_x_range: tuple[float, float] = (-0.4, 0.4)
object_z_range: tuple[float, float] = (0.0, 0.7)
object_angle_range: tuple[float, float] = (0.0, 6.28318531)
object_x_vel_range: tuple[float, float] = (-5.0, 5.0)
min_object_hand_dist: float = 0.08
# Physics settling at episode start (in control steps, i.e. ctrl_dt units).
# Internally this will be converted to `settle_steps * sim_substeps` physics steps.
settle_steps: int = 80
settle_zero_vel: bool = True
# BringBall reward shaping.
lift_height_threshold: float = 0.04
touch_threshold: float = 0.01
side_penalty_scale: float = 0.05
side_penalty_tanh_scale: float = 10.0
hover_penalty_scale: float = 0.02
hover_close_threshold: float = 0.1
post_grasp_discount: float = 0.7
lift_height_weight: float = 0.3
transport_weight: float = 0.7
transport_progress_scale: float = 0.0
transport_progress_clip: float = 0.02
precision_weight: float = 0.0
precision_margin: float = 0.02
precision_value_at_margin: float = 0.1
# BringBall-specific overrides.
settle_steps: int = 300
p_in_hand: float = 0.0
p_in_target: float = 0.0
randomize_arm: bool = False
object_z_range: tuple[float, float] = (0.2, 0.7)
object_x_vel_range: tuple[float, float] = (0.0, 0.0)
hover_penalty_scale: float = 0.03
post_grasp_discount: float = 0.0
lift_height_weight: float = 0.1
transport_weight: float = 2.0
transport_progress_scale: float = 2.0
transport_progress_clip: float = 0.02
precision_weight: float = 1.0
precision_margin: float = 0.01
# Reward component weights (total reward mixing).
reach_weight: float = 1.0
orient_weight: float = 1.5
pause_weight: float = 0.5
close_weight: float = 2.0
lift_reward_weight: float = 6.0

View File

@@ -0,0 +1,171 @@
<mujoco model="planar manipulator - bring ball">
<include file="../../common/visual.xml"/>
<include file="../../common/skybox.xml"/>
<include file="../../common/materials.xml"/>
<asset>
<texture name="background" type="2d" file="../../common/motphys-ground.png"/>
<material name="background" texture="background" texrepeat="1 1" texuniform="true"/>
</asset>
<visual>
<map shadowclip=".5"/>
<quality shadowsize="2048"/>
</visual>
<option timestep="0.001" cone="elliptic"/>
<default>
<geom friction=".7" solimp="0.9 0.97 0.001" solref=".005 1"/>
<joint solimplimit="0 0.99 0.01" solreflimit=".005 1"/>
<general ctrllimited="true"/>
<tendon width="0.01"/>
<site size=".003 .003 .003" material="site" group="3"/>
<default class="arm">
<geom type="capsule" material="self" density="500"/>
<joint type="hinge" pos="0 0 0" axis="0 -1 0" limited="true"/>
<default class="hand">
<joint damping=".5" range="-10 60"/>
<geom size=".008"/>
<site type="box" size=".018 .005 .005" pos=".022 0 -.002" euler="0 15 0" group="4"/>
<default class="fingertip">
<geom type="sphere" size=".008" material="effector"/>
<joint damping=".01" stiffness=".01" range="-40 20"/>
<site size=".012 .005 .008" pos=".003 0 .003" group="4" euler="0 0 0"/>
</default>
</default>
</default>
<default class="object">
<geom material="self"/>
</default>
<default class="task">
<site rgba="0 0 0 0"/>
</default>
<default class="obstacle">
<geom material="decoration" friction="0"/>
</default>
<default class="ghost">
<geom material="target" contype="0" conaffinity="0"/>
</default>
</default>
<worldbody>
<!-- Arena -->
<light name="light" directional="true" diffuse=".6 .6 .6" pos="0 0 1" dir="0 0.1 -1" specular=".3 .3 .3"/>
<geom name="floor" type="plane" pos="0 0 0" size=".4 .2 10" material="background"/>
<geom name="wall1" type="plane" pos="-.682843 0 .282843" size=".4 .2 10" material="background" zaxis="1 0 1" contype="0" conaffinity="0"/>
<geom name="wall2" type="plane" pos=".682843 0 .282843" size=".4 .2 10" material="background" zaxis="-1 0 1" contype="0" conaffinity="0"/>
<geom name="wall1_collider" type="box" pos="-.7394115 0 .2262745" size="5 .2 .08" quat="0.9238795 0 0.3826834 0" contype="1" conaffinity="1" rgba="0 0 0 0.001"/>
<geom name="wall2_collider" type="box" pos=".7394115 0 .2262745" size="5 .2 .08" quat="0.9238795 0 -0.3826834 0" contype="1" conaffinity="1" rgba="0 0 0 0.001"/>
<geom name="background" type="plane" pos="0 .2 .5" size="1 .5 10" material="background" zaxis="0 -1 0" contype="0" conaffinity="0"/>
<camera name="fixed" pos="0 -16 .4" xyaxes="1 0 0 0 0 1" fovy="4"/>
<!-- Arm -->
<geom name="arm_root" type="cylinder" fromto="0 -.022 .4 0 .022 .4" size=".024"
material="decoration" contype="0" conaffinity="0"/>
<body name="upper_arm" pos="0 0 .4" childclass="arm">
<joint name="arm_root" damping="2" limited="false"/>
<geom name="upper_arm" size=".02" fromto="0 0 0 0 0 .18"/>
<body name="middle_arm" pos="0 0 .18" childclass="arm">
<joint name="arm_shoulder" damping="1.5" range="-160 160"/>
<geom name="middle_arm" size=".017" fromto="0 0 0 0 0 .15"/>
<body name="lower_arm" pos="0 0 .15">
<joint name="arm_elbow" damping="1" range="-160 160"/>
<geom name="lower_arm" size=".014" fromto="0 0 0 0 0 .12"/>
<body name="hand" pos="0 0 .12">
<joint name="arm_wrist" damping=".5" range="-140 140" />
<geom name="hand" size=".011" fromto="0 0 0 0 0 .03"/>
<geom name="palm1" fromto="0 0 .03 .03 0 .045" class="hand"/>
<geom name="palm2" fromto="0 0 .03 -.03 0 .045" class="hand"/>
<site name="grasp" pos="0 0 .065"/>
<body name="pinch site" pos="0 0 .090">
<site name="pinch"/>
<inertial pos="0 0 0" mass="1e-6" diaginertia="1e-12 1e-12 1e-12"/>
<camera name="hand" pos="0 -.3 0" xyaxes="1 0 0 0 0 1" mode="track"/>
</body>
<site name="palm_touch" type="box" group="4" size=".025 .005 .008" pos="0 0 .043"/>
<body name="thumb" pos=".03 0 .045" euler="0 -90 0" childclass="hand">
<joint name="thumb"/>
<geom name="thumb1" fromto="0 0 0 .02 0 -.01" size=".007"/>
<geom name="thumb2" fromto=".02 0 -.01 .04 0 -.01" size=".007"/>
<site name="thumb_touch" group="4"/>
<body name="thumbtip" pos=".05 0 -.01" childclass="fingertip">
<joint name="thumbtip"/>
<geom name="thumbtip1" pos="-.003 0 0" />
<geom name="thumbtip2" pos=".003 0 0" />
<site name="thumbtip_touch" group="4"/>
</body>
</body>
<body name="finger" pos="-.03 0 .045" euler="0 90 180" childclass="hand">
<joint name="finger"/>
<geom name="finger1" fromto="0 0 0 .02 0 -.01" size=".007" />
<geom name="finger2" fromto=".02 0 -.01 .04 0 -.01" size=".007"/>
<site name="finger_touch"/>
<body name="fingertip" pos=".05 0 -.01" childclass="fingertip">
<joint name="fingertip"/>
<geom name="fingertip1" pos="-.003 0 0" />
<geom name="fingertip2" pos=".003 0 0" />
<site name="fingertip_touch"/>
</body>
</body>
</body>
</body>
</body>
</body>
<!-- prop: ball -->
<body name="ball" pos=".4 0 .4" childclass="object">
<joint name="ball_x" type="slide" axis="1 0 0" ref=".4"/>
<joint name="ball_z" type="slide" axis="0 0 1" ref=".4"/>
<joint name="ball_y" type="hinge" axis="0 1 0"/>
<geom name="ball" type="sphere" size=".022" />
<site name="ball" type="sphere"/>
</body>
<!-- target -->
<body name="target_ball" pos=".4 .001 .4" mocap="true" childclass="ghost">
<geom name="target_ball" type="sphere" size=".02" />
<site name="target_ball" type="sphere"/>
</body>
</worldbody>
<tendon>
<fixed name="grasp">
<joint joint="thumb" coef=".5"/>
<joint joint="finger" coef=".5"/>
</fixed>
<fixed name="coupling">
<joint joint="thumb" coef="-.5"/>
<joint joint="finger" coef=".5"/>
</fixed>
</tendon>
<equality>
<tendon name="coupling" tendon1="coupling" solimp="0.95 0.99 0.001" solref=".005 .5"/>
</equality>
<sensor>
<touch name="palm_touch" site="palm_touch"/>
<touch name="finger_touch" site="finger_touch"/>
<touch name="thumb_touch" site="thumb_touch"/>
<touch name="fingertip_touch" site="fingertip_touch"/>
<touch name="thumbtip_touch" site="thumbtip_touch"/>
</sensor>
<actuator>
<motor name="root" joint="arm_root" ctrlrange="-1 1" gear="12"/>
<motor name="shoulder" joint="arm_shoulder" ctrlrange="-1 1" gear="8"/>
<motor name="elbow" joint="arm_elbow" ctrlrange="-1 1" gear="4"/>
<motor name="wrist" joint="arm_wrist" ctrlrange="-1 1" gear="2"/>
<motor name="grasp" tendon="grasp" ctrlrange="-1 1" gear="2"/>
</actuator>
</mujoco>

View File

@@ -0,0 +1,579 @@
# 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.manipulator.cfg import BringBallCfg
from motrix_envs.math import quaternion
from motrix_envs.np import reward as reward_utils
from motrix_envs.np.env import NpEnv, NpEnvState
_ARM_JOINTS = (
"arm_root",
"arm_shoulder",
"arm_elbow",
"arm_wrist",
"finger",
"fingertip",
"thumb",
"thumbtip",
)
_TOUCH_SENSORS = ("palm_touch", "finger_touch", "thumb_touch", "fingertip_touch", "thumbtip_touch")
_HAND_GEOMS = (
"hand",
"palm1",
"palm2",
"thumb1",
"thumb2",
"thumbtip1",
"thumbtip2",
"finger1",
"finger2",
"fingertip1",
"fingertip2",
)
def _sanitize_joint_limits(low: np.ndarray, high: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
low = low.copy()
high = high.copy()
low = np.where(np.isfinite(low), low, -np.pi)
high = np.where(np.isfinite(high), high, np.pi)
return low.astype(np.float32), high.astype(np.float32)
def _quat_from_y_angle(angle: np.ndarray) -> np.ndarray:
zeros = np.zeros_like(angle)
return quaternion.from_euler(zeros, angle, zeros)
def _quat_to_z_axis(quat: np.ndarray) -> np.ndarray:
quat = np.asarray(quat, dtype=np.float32)
return quaternion.rotate_vector(quat, np.array([0.0, 0.0, 1.0], dtype=np.float32)).astype(np.float32)
def _tolerance(
x: np.ndarray,
*,
bounds: tuple[float, float] = (0.0, 0.0),
margin: float = 0.0,
sigmoid: str = "gaussian",
value_at_margin: float = 0.1,
) -> np.ndarray:
"""Vectorized tolerance reward (ported from dm_control-style reward_utils)."""
return reward_utils.tolerance(x, bounds=bounds, margin=margin, sigmoid=sigmoid, value_at_margin=value_at_margin)
class ManipulatorBase(NpEnv):
_cfg: BringBallCfg
_observation_space: gym.spaces.Box
_action_space: gym.spaces.Box
def __init__(self, cfg: BringBallCfg, num_envs: int = 1):
super().__init__(cfg, num_envs=num_envs)
self._cfg = cfg
self._joint_limit_low, self._joint_limit_high = _sanitize_joint_limits(*self._model.joint_limits)
self._arm_joint_pos_indices = np.array([self._joint_pos_index(n) for n in _ARM_JOINTS], dtype=np.int32)
self._arm_joint_vel_indices = np.array([self._joint_vel_index(n) for n in _ARM_JOINTS], dtype=np.int32)
self._thumb_qpos_i = self._joint_pos_index("thumb")
self._finger_qpos_i = self._joint_pos_index("finger")
self._thumbtip_qpos_i = self._joint_pos_index("thumbtip")
self._fingertip_qpos_i = self._joint_pos_index("fingertip")
self._grasp_site = self._model.get_site("grasp")
# Ensure correct actuator index is retrieved from base model
self._grasp_act_i = int(self._model.get_actuator_index("grasp"))
self._object_site = self._model.get_site("ball")
self._target_site = self._model.get_site("target_ball")
target_body = self._model.get_body("target_ball")
if target_body is None:
raise ValueError("Target body 'target_ball' not found in model")
self._target_mocap = target_body.mocap
object_qpos_joints = ("ball_x", "ball_z", "ball_y")
object_geom_names = ("ball",)
self._object_qpos_indices = np.array([self._joint_pos_index(n) for n in object_qpos_joints], dtype=np.int32)
self._object_qvel_indices = np.array([self._joint_vel_index(n) for n in object_qpos_joints], dtype=np.int32)
self._object_x_qvel_i = int(self._object_qvel_indices[0])
self._hand_geom_indices = np.array([self._model.get_geom_index(name) for name in _HAND_GEOMS], dtype=np.uint32)
object_geom_indices = np.array(
[self._model.get_geom_index(name) for name in object_geom_names], dtype=np.uint32
)
self._hand_object_pairs = np.stack(
[
np.repeat(self._hand_geom_indices, object_geom_indices.shape[0]),
np.tile(object_geom_indices, self._hand_geom_indices.shape[0]),
],
axis=-1,
).astype(np.uint32)
self._num_hand_object_pairs = int(self._hand_object_pairs.shape[0])
self._init_dof_pos = self._model.compute_init_dof_pos().astype(np.float32)
self._init_dof_vel = np.zeros((self._model.num_dof_vel,), dtype=np.float32)
self._init_action_space()
self._init_obs_space()
def _joint_pos_index(self, joint_name: str) -> int:
joint_index = self._model.get_joint_index(joint_name)
return int(self._model.joint_dof_pos_indices[joint_index])
def _joint_vel_index(self, joint_name: str) -> int:
joint_index = self._model.get_joint_index(joint_name)
return int(self._model.joint_dof_vel_indices[joint_index])
def _init_action_space(self):
low, high = self._model.actuator_ctrl_limits
self._action_space = gym.spaces.Box(low, high, (self._model.num_actuators,), dtype=np.float32)
def _init_obs_space(self):
# arm_pos(sin,cos)=16 + arm_vel=8 + touch=5 + hand=3 + object=3 + target=3 + rel=3
self._observation_space = gym.spaces.Box(-np.inf, np.inf, (41,), 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: np.ndarray, state: NpEnvState) -> NpEnvState:
actions = np.asarray(actions, dtype=np.float32)
# Enforce actuator control limits to avoid out-of-range impulses.
actions = np.clip(actions, self._action_space.low, self._action_space.high).astype(np.float32)
state.info["last_actions"] = state.info["actions"]
state.info["actions"] = actions
state.data.actuator_ctrls = actions
return state
def _touch_raw(self, data: mtx.SceneData) -> np.ndarray:
values = []
for name in _TOUCH_SENSORS:
v = np.asarray(self._model.get_sensor_value(name, data)).reshape(data.shape[0], -1)[:, 0]
values.append(v)
return np.stack(values, axis=-1).astype(np.float32)
def _touch_log(self, data: mtx.SceneData) -> np.ndarray:
return np.log1p(self._touch_raw(data))
def _hand_pos(self, data: mtx.SceneData) -> np.ndarray:
return self._grasp_site.get_position(data).astype(np.float32)
def _object_pos(self, data: mtx.SceneData) -> np.ndarray:
return self._object_site.get_position(data).astype(np.float32)
def _target_pos(self, data: mtx.SceneData) -> np.ndarray:
return self._target_site.get_position(data).astype(np.float32)
def _contact_with_object(self, data: mtx.SceneData) -> np.ndarray:
cquery = self._model.get_contact_query(data)
colliding = cquery.is_colliding(self._hand_object_pairs)
colliding = np.asarray(colliding).reshape((data.shape[0], self._num_hand_object_pairs))
return colliding.any(axis=-1)
def _get_obs(self, data: mtx.SceneData) -> np.ndarray:
qpos = data.dof_pos[:, self._arm_joint_pos_indices]
arm_pos = np.stack([np.sin(qpos), np.cos(qpos)], axis=-1).reshape(data.shape[0], -1)
arm_vel = data.dof_vel[:, self._arm_joint_vel_indices]
touch = self._touch_log(data)
hand_pos = self._hand_pos(data)
object_pos = self._object_pos(data)
target_pos = self._target_pos(data)
rel = object_pos - target_pos
obs = np.concatenate([arm_pos, arm_vel, touch, hand_pos, object_pos, target_pos, rel], axis=-1)
assert obs.shape == (data.shape[0], self._observation_space.shape[0])
return obs.astype(np.float32)
def _sample_arm_joint_angles(self, num: int) -> np.ndarray:
joint_indices = np.array([self._model.get_joint_index(n) for n in _ARM_JOINTS], dtype=np.int32)
low = self._joint_limit_low[joint_indices]
high = self._joint_limit_high[joint_indices]
return np.random.uniform(low=low, high=high, size=(num, joint_indices.shape[0])).astype(np.float32)
def _sample_target_pose(self, num: int) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
cfg = self._cfg
target_x = np.random.uniform(cfg.target_x_range[0], cfg.target_x_range[1], size=(num,)).astype(np.float32)
target_z = np.random.uniform(cfg.target_z_range[0], cfg.target_z_range[1], size=(num,)).astype(np.float32)
target_angle = np.random.uniform(cfg.target_angle_range[0], cfg.target_angle_range[1], size=(num,)).astype(
np.float32
)
return target_x, target_z, target_angle
def _set_target_mocap(
self,
data: mtx.SceneData,
target_x: np.ndarray,
target_z: np.ndarray,
target_angle: np.ndarray,
):
pose = np.zeros((data.shape[0], 7), dtype=np.float32)
pose[:, 0] = target_x
pose[:, 1] = float(self._cfg.target_y)
pose[:, 2] = target_z
pose[:, 3:7] = _quat_from_y_angle(target_angle)
self._target_mocap.set_pose(data, pose)
def _set_object_state(
self,
dof_pos: np.ndarray,
dof_vel: np.ndarray,
target_x: np.ndarray,
target_z: np.ndarray,
target_angle: np.ndarray,
grasp_pos: np.ndarray,
):
cfg = self._cfg
num = dof_pos.shape[0]
# Default: uniform in workspace.
object_x = np.random.uniform(cfg.object_x_range[0], cfg.object_x_range[1], size=(num,)).astype(np.float32)
object_z = np.random.uniform(cfg.object_z_range[0], cfg.object_z_range[1], size=(num,)).astype(np.float32)
object_angle = np.random.uniform(cfg.object_angle_range[0], cfg.object_angle_range[1], size=(num,)).astype(
np.float32
)
# dm_control-style object init distribution.
r = np.random.uniform(0.0, 1.0, size=(num,)).astype(np.float32)
in_hand = r < float(cfg.p_in_hand)
in_target = (r >= float(cfg.p_in_hand)) & (r < float(cfg.p_in_hand + cfg.p_in_target))
uniform = ~(in_hand | in_target)
# Avoid initializing the object too close to the hand to prevent interpenetration / impulse explosions.
min_dist = float(getattr(cfg, "min_object_hand_dist", 0.0))
if min_dist > 0.0 and uniform.any():
min_dist_sq = np.float32(min_dist * min_dist)
max_attempts = 50
pending = uniform.copy()
for _ in range(max_attempts):
if not pending.any():
break
dx = object_x - grasp_pos[:, 0]
dz = object_z - grasp_pos[:, 2]
too_close = (dx * dx + dz * dz) < min_dist_sq
pending = pending & too_close
if not pending.any():
break
n = int(pending.sum())
object_x[pending] = np.random.uniform(cfg.object_x_range[0], cfg.object_x_range[1], size=(n,)).astype(
np.float32
)
object_z[pending] = np.random.uniform(cfg.object_z_range[0], cfg.object_z_range[1], size=(n,)).astype(
np.float32
)
object_x[in_target] = target_x[in_target]
object_z[in_target] = target_z[in_target]
object_angle[in_target] = target_angle[in_target]
object_x[in_hand] = grasp_pos[in_hand, 0]
object_z[in_hand] = grasp_pos[in_hand, 2]
object_angle[in_hand] = 0.0
dof_pos[:, self._object_qpos_indices] = np.stack([object_x, object_z, object_angle], axis=-1)
dof_vel[:, self._object_qvel_indices] = 0.0
if uniform.any():
dof_vel[uniform, self._object_x_qvel_i] = np.random.uniform(
cfg.object_x_vel_range[0], cfg.object_x_vel_range[1], size=(int(uniform.sum()),)
).astype(np.float32)
def _settle(self, data: mtx.SceneData):
control_steps = int(self._cfg.settle_steps)
if control_steps <= 0:
return
substeps = int(self._cfg.sim_substeps)
physics_steps = control_steps * max(substeps, 1)
data.actuator_ctrls = np.zeros((data.shape[0], self._model.num_actuators), dtype=np.float32)
for _ in range(physics_steps):
self._model.step(data)
if self._cfg.settle_zero_vel:
data.set_dof_vel(np.zeros((data.shape[0], self._model.num_dof_vel), dtype=np.float32))
self._model.forward_kinematic(data)
def initialize_episode(self, data: mtx.SceneData) -> None:
"""Episode initialization with optional physics settling (dm_control-style)."""
num = int(data.shape[0])
dof_pos = np.tile(self._init_dof_pos, (num, 1))
dof_vel = np.tile(self._init_dof_vel, (num, 1))
# Optionally randomize arm joint angles and symmetrize the hand.
if getattr(self._cfg, "randomize_arm", True):
arm_angles = self._sample_arm_joint_angles(num)
dof_pos[:, self._arm_joint_pos_indices] = arm_angles
dof_pos[:, self._finger_qpos_i] = dof_pos[:, self._thumb_qpos_i]
dof_pos[:, self._fingertip_qpos_i] = dof_pos[:, self._thumbtip_qpos_i]
data.reset(self._model)
data.set_dof_vel(dof_vel)
data.set_dof_pos(dof_pos, self._model)
self._model.forward_kinematic(data)
target_x, target_z, target_angle = self._sample_target_pose(num)
self._set_target_mocap(data, target_x, target_z, target_angle)
self._model.forward_kinematic(data)
grasp_pos = self._grasp_site.get_position(data)
self._set_object_state(dof_pos, dof_vel, target_x, target_z, target_angle, grasp_pos)
data.set_dof_vel(dof_vel)
data.set_dof_pos(dof_pos, self._model)
self._model.forward_kinematic(data)
arm_qpos = data.dof_pos[:, self._arm_joint_pos_indices].copy()
self._settle(data)
if not getattr(self._cfg, "randomize_arm", True):
dof_pos_after = data.dof_pos.copy()
dof_vel_after = data.dof_vel.copy()
dof_pos_after[:, self._arm_joint_pos_indices] = arm_qpos
dof_vel_after[:, self._arm_joint_vel_indices] = 0.0
data.set_dof_pos(dof_pos_after, self._model)
data.set_dof_vel(dof_vel_after)
self._model.forward_kinematic(data)
def reset(self, data: mtx.SceneData) -> tuple[np.ndarray, dict]:
num = int(data.shape[0])
self.initialize_episode(data)
obs = self._get_obs(data)
info = {
"actions": np.zeros((num, self._model.num_actuators), dtype=np.float32),
"last_actions": np.zeros((num, self._model.num_actuators), dtype=np.float32),
}
return obs, info
@registry.env("dm-manipulator-bring-ball", "np")
class BringBall(ManipulatorBase):
_cfg: BringBallCfg
def __init__(self, cfg: BringBallCfg, num_envs: int = 1):
super().__init__(cfg, num_envs=num_envs)
# 1. Sensors setup
self._fingertip_site = self._model.get_site("fingertip_touch")
self._thumbtip_site = self._model.get_site("thumbtip_touch")
self._touch_idx_palm = _TOUCH_SENSORS.index("palm_touch")
self._touch_idx_fingertip = _TOUCH_SENSORS.index("fingertip_touch")
self._touch_idx_thumbtip = _TOUCH_SENSORS.index("thumbtip_touch")
def _compute_hand_direction(self, data: mtx.SceneData) -> np.ndarray:
"""Calculates the Z-axis vector of the hand (grasp site)."""
grasp_pose = self._grasp_site.get_pose(data)
return _quat_to_z_axis(grasp_pose[:, 3:])
def _get_tip_positions(self, data: mtx.SceneData) -> tuple[np.ndarray, np.ndarray]:
fingertip_pos = self._fingertip_site.get_position(data).astype(np.float32)
thumbtip_pos = self._thumbtip_site.get_position(data).astype(np.float32)
return fingertip_pos, thumbtip_pos
def _compute_aim_direction(self, object_pos: np.ndarray, grasp_pos: np.ndarray) -> np.ndarray:
vec_to_aim = object_pos - grasp_pos
dist_to_aim = np.linalg.norm(vec_to_aim, axis=-1, keepdims=True)
return vec_to_aim / (dist_to_aim + 1e-6)
def _strict_grasp_condition(self, data: mtx.SceneData, object_pos: np.ndarray) -> np.ndarray:
cfg = self._cfg
height_ok = object_pos[:, 2] > float(cfg.lift_height_threshold)
all_touch = self._touch_raw(data)
touch_threshold = float(cfg.touch_threshold)
touch_ok = (
(all_touch[..., self._touch_idx_palm] > touch_threshold)
| (all_touch[..., self._touch_idx_fingertip] > touch_threshold)
| (all_touch[..., self._touch_idx_thumbtip] > touch_threshold)
)
object_contact_ok = self._contact_with_object(data)
return height_ok & touch_ok & object_contact_ok
def update_state(self, state: NpEnvState) -> NpEnvState:
data = state.data
cfg = self._cfg
# 1. Observation
obs = self._get_obs(data)
terminated = np.isnan(obs).any(axis=-1)
# 2. Positions
object_pos = self._object_pos(data)
target_pos = self._target_pos(data)
grasp_pos = self._hand_pos(data)
# 3. Kinematics
fingertip_pos, thumbtip_pos = self._get_tip_positions(data)
dist_finger = np.linalg.norm(fingertip_pos - object_pos, axis=-1)
dist_thumb = np.linalg.norm(thumbtip_pos - object_pos, axis=-1)
avg_tip_dist = ((dist_finger + dist_thumb) / 2.0).astype(np.float32)
move_dist = np.linalg.norm(object_pos - target_pos, axis=-1).astype(np.float32)
# 4. Dynamics
arm_vel = data.dof_vel[:, self._arm_joint_vel_indices[:4]].astype(np.float32)
arm_speed = np.linalg.norm(arm_vel, axis=-1).astype(np.float32)
arm_speed_step = (arm_speed * float(cfg.ctrl_dt)).astype(np.float32)
# 5. Logic Checks
is_grasped = self._strict_grasp_condition(data, object_pos)
contact_with_obj = self._contact_with_object(data)
hover_threshold = float(cfg.hover_close_threshold)
is_close_to_ball = (avg_tip_dist < hover_threshold).astype(np.float32)
grasp_mask = is_grasped.astype(np.float32)
post_grasp_scale = 1.0 - grasp_mask * float(cfg.post_grasp_discount)
# --- Rewards ---
# R1: Reach
r_reach = _tolerance(avg_tip_dist, bounds=(0.0, 0.02), margin=0.25, sigmoid="linear").astype(np.float32)
r_reach = (r_reach * post_grasp_scale).astype(np.float32)
# R2: Orient
hand_dir = self._compute_hand_direction(data)
unit_vec_to_aim = self._compute_aim_direction(object_pos, grasp_pos)
pointing_dot = np.sum(hand_dir * unit_vec_to_aim, axis=-1)
# Dynamic tolerance
dist_from_base = np.linalg.norm(object_pos[:, :2], axis=-1)
orient_bound_lower = 0.95 * np.clip(dist_from_base / 0.5, 0.0, 1.0)
r_orient_raw = 1.0 - orient_bound_lower + pointing_dot
r_orient = np.clip(r_orient_raw, 0.0, 1.0).astype(np.float32)
r_orient = (r_orient * post_grasp_scale).astype(np.float32)
# R3: Pause
r_pause = (
_tolerance(arm_speed_step, bounds=(0.0, 0.05), margin=0.3, sigmoid="linear").astype(np.float32)
* is_close_to_ball
)
r_pause = (r_pause * post_grasp_scale).astype(np.float32)
# R4: Close
default_actions = np.zeros((data.shape[0], self._model.num_actuators), dtype=np.float32)
grasp_action = state.info.get("actions", default_actions)[:, self._grasp_act_i].astype(np.float32)
r_close_intent = _tolerance(
grasp_action, bounds=(0.8, 1.0), margin=1.0, sigmoid="linear", value_at_margin=0.01
).astype(np.float32)
r_approach_grasp = r_close_intent * is_close_to_ball * r_orient * r_pause * contact_with_obj.astype(np.float32)
r_sustain_grasp = r_close_intent * grasp_mask
r_close = (r_approach_grasp * (1.0 - grasp_mask) + r_sustain_grasp).astype(np.float32)
# R5: Lift & Transport
lift_h = float(cfg.lift_height_threshold)
ball_z = object_pos[:, 2].astype(np.float32)
r_lift_height = (
_tolerance(ball_z, bounds=(lift_h, lift_h + 0.15), margin=0.02, sigmoid="linear", value_at_margin=0.01)
* grasp_mask
).astype(np.float32)
r_transport = (_tolerance(move_dist, bounds=(0.0, 0.01), margin=0.3, sigmoid="linear") * grasp_mask).astype(
np.float32
)
r_precision = (
_tolerance(
move_dist,
bounds=(0.0, 0.0),
margin=float(cfg.precision_margin),
sigmoid="gaussian",
value_at_margin=float(cfg.precision_value_at_margin),
)
* grasp_mask
).astype(np.float32)
lift_height_weight = float(cfg.lift_height_weight)
transport_weight = float(cfg.transport_weight)
lift_norm = max(lift_height_weight + transport_weight, 1e-6)
r_lift = ((lift_height_weight * r_lift_height + transport_weight * r_transport) / lift_norm).astype(np.float32)
prev_move_dist = state.info.get("prev_move_dist")
if prev_move_dist is None:
prev_move_dist = move_dist
else:
prev_move_dist = np.asarray(prev_move_dist, dtype=np.float32)
if "steps" in state.info:
first_step = state.info["steps"] == 0
prev_move_dist = np.where(first_step, move_dist, prev_move_dist)
progress_clip = float(cfg.transport_progress_clip)
progress = (prev_move_dist - move_dist) / max(progress_clip, 1e-6)
progress = np.clip(progress, -1.0, 1.0).astype(np.float32)
r_progress = (progress * float(cfg.transport_progress_scale) * grasp_mask).astype(np.float32)
state.info["prev_move_dist"] = move_dist.astype(np.float32)
# --- Penalties ---
all_touch = self._touch_raw(data)
side_touch_sum = (all_touch[..., 1] + all_touch[..., 2]).astype(np.float32)
penalty_side = (
-float(cfg.side_penalty_scale) * np.tanh(side_touch_sum * float(cfg.side_penalty_tanh_scale))
).astype(np.float32)
hover_phase = (is_close_to_ball > 0.5) & (~contact_with_obj)
penalty_hover = (-float(cfg.hover_penalty_scale) * hover_phase.astype(np.float32)).astype(np.float32)
# --- Total ---
reach_w = float(cfg.reach_weight)
orient_w = float(cfg.orient_weight)
pause_w = float(cfg.pause_weight)
close_w = float(cfg.close_weight)
lift_w = float(cfg.lift_reward_weight)
precision_w = float(cfg.precision_weight)
weight_sum = max(reach_w + orient_w + pause_w + close_w + lift_w + precision_w, 1e-6)
reward = (
(
reach_w * r_reach
+ orient_w * r_orient
+ pause_w * r_pause
+ close_w * r_close
+ lift_w * r_lift
+ precision_w * r_precision
)
/ weight_sum
+ penalty_side
+ penalty_hover
+ r_progress
).astype(np.float32)
reward = np.where(terminated, 0.0, reward)
state.info["Reward"] = {
"reach": r_reach,
"orient": r_orient,
"close": r_close,
"lift": r_lift,
"transport": r_transport,
"precision": r_precision,
"progress": r_progress,
"total": reward,
}
state.info["metrics"] = {
"pointing_dot": pointing_dot,
"is_grasped": is_grasped.astype(np.float32),
"avg_tip_dist": avg_tip_dist,
"move_dist": move_dist,
"transport_reward": r_transport,
"precision_reward": r_precision,
"progress_reward": r_progress,
}
return state.replace(obs=obs, reward=reward, terminated=terminated)

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 pendulum_np # noqa: F401

View File

@@ -0,0 +1,39 @@
# 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
import numpy as np
from motrix_envs import registry
from motrix_envs.base import EnvCfg
model_file = os.path.dirname(__file__) + "/pendulum.xml"
@registry.envcfg("pendulum")
@dataclass
class PendulumEnvCfg(EnvCfg):
model_file: str = model_file
max_episode_seconds: float = 10.0
sim_dt: float = 0.0125
ctrl_dt: float = 0.025
angle_bound: float = 8.0
cosing_bound: float = 0.0
# reset_noise_scale: float = 0.01
def __post_init__(self):
self.cosing_bound = float(np.cos(np.deg2rad(self.angle_bound)))

View File

@@ -0,0 +1,35 @@
<mujoco model="pendulum">
<include file="../../common/visual.xml" />
<include file="../../common/skybox.xml" />
<include file="../../common/materials.xml" />
<asset>
<texture name="motphys_ground_tex" type="2d" file="../../common/motphys-ground.png" />
<material name="motphys_ground_mat" texture="motphys_ground_tex" texrepeat="1 1" texuniform="true" />
</asset>
<option timestep="0.02">
<flag contact="disable" energy="enable"/>
</option>
<worldbody>
<light diffuse="0.6 0.6 0.6" pos="0 0 1.5"
dir="-0.49835488200187683 0.2925136387348175 -0.8161361217498779" directional="true" />
<geom name="floor" size="0 0 0.2" type="plane" material="motphys_ground_mat"/>
<camera name="fixed" pos="0 -1.5 2" xyaxes='1 0 0 0 1 1'/>
<camera name="lookat" mode="targetbodycom" target="pole" pos="0 -2 1"/>
<body name="pole" pos="0 0 .6">
<joint name="hinge" type="hinge" axis="0 1 0" damping="0.1"/>
<geom name="base" material="decoration" type="cylinder" fromto="0 -.03 0 0 .03 0" size="0.021" mass="0"/>
<geom name="pole" material="self" type="capsule" fromto="0 0 0 0 0 0.5" size="0.02" mass="0"/>
<geom name="mass" material="effector" type="sphere" pos="0 0 0.5" size="0.05" mass="1"/>
</body>
</worldbody>
<actuator>
<motor name="torque" joint="hinge" gear="5" ctrlrange="-1 1" ctrllimited="true"/>
</actuator>
</mujoco>

View File

@@ -0,0 +1,117 @@
# 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 import reward as reward_utils
from motrix_envs.np.env import NpEnv, NpEnvState
from .cfg import PendulumEnvCfg
@registry.env("pendulum", "np")
class PendulumEnv(NpEnv):
_cfg: PendulumEnvCfg
def __init__(self, cfg: PendulumEnvCfg, num_envs: int = 1):
super().__init__(cfg, num_envs=num_envs)
ctrl_limits = self._model.actuator_ctrl_limits
self._action_low = float(ctrl_limits[0, 0])
self._action_high = float(ctrl_limits[1, 0])
self._action_space = gym.spaces.Box(-1.0, 1.0, (1,), dtype=np.float32)
self._observation_space = gym.spaces.Box(-np.inf, np.inf, (3,), 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):
actions = np.clip(actions, -1.0, 1.0)
scaled = self._action_low + (actions + 1.0) * 0.5 * (self._action_high - self._action_low)
state.data.actuator_ctrls = scaled
return state
def update_state(self, state: NpEnvState):
# compute observation
data = state.data
dof_pos = data.dof_pos
dof_vel = data.dof_vel
angle = dof_pos[:, 0]
ang_vel = dof_vel[:, 0]
obs = np.stack([np.cos(angle), np.sin(angle), ang_vel], axis=-1)
assert obs.shape == (self._num_envs, 3)
# compute reward
angle_wrapped = (angle + np.pi) % (2 * np.pi) - np.pi
ctrl = data.actuator_ctrls[:, 0]
# In this model, zero angle corresponds to the hanging-down position.
# Shift the target by pi to encourage the upright (inverted) posture.
upright = (1.0 + np.cos(angle_wrapped)) * 0.5
prev_ctrl = state.info.get("prev_ctrl", np.zeros_like(ctrl))
ctrl_delta = ctrl - prev_ctrl
vel_penalty = 0.2 * (ang_vel**2)
energy = 0.5 * ang_vel**2 + (1.0 - np.cos(angle_wrapped))
energy_target = 2.0
energy_reward = reward_utils.tolerance(
energy,
bounds=(energy_target, energy_target),
margin=2.0,
value_at_margin=0.1,
sigmoid="gaussian",
)
reward = (3.0 * upright + energy_reward - vel_penalty - 0.001 * ctrl**2 - 0.001 * ctrl_delta**2).astype(
np.float32
)
# compute terminated
terminated = np.isnan(obs).any(axis=-1)
state.obs = obs
state.reward = reward
state.terminated = terminated
state.info["prev_ctrl"] = ctrl
return state
def reset(self, data: mtx.SceneData):
cfg: PendulumEnvCfg = self._cfg
reset_noise_scale = getattr(cfg, "reset_noise_scale", 0.0)
num_reset = data.shape[0]
dof_pos = np.zeros((num_reset, self._num_dof_pos), dtype=np.float32)
dof_vel = np.zeros((num_reset, self._num_dof_vel), dtype=np.float32)
dof_pos[:, 0] = np.random.uniform(-np.pi, np.pi, size=(num_reset,))
if reset_noise_scale > 0.0:
dof_vel[:, 0] = np.random.uniform(-reset_noise_scale, reset_noise_scale, size=(num_reset,))
data.reset(self._model)
data.set_dof_vel(dof_vel)
data.set_dof_pos(dof_pos, self._model)
angle = dof_pos[:, 0]
ang_vel = dof_vel[:, 0]
obs = np.stack([np.cos(angle), np.sin(angle), ang_vel], axis=-1)
return obs, {"prev_ctrl": np.zeros((num_reset,), dtype=np.float32)}

View File

@@ -19,7 +19,7 @@ import motrixsim as mtx
import numpy as np
from motrix_envs import registry
from motrix_envs.math.quaternion import Quaternion
from motrix_envs.math import quaternion
from motrix_envs.np.env import NpEnv, NpEnvState
from .cfg import AnymalCEnvCfg
@@ -185,7 +185,7 @@ class AnymalCEnv(NpEnv):
# Get commands - convert to relative velocity commands
pose_commands = state.info["pose_commands"]
robot_position = root_pos[:, :2]
robot_heading = Quaternion.get_yaw(root_quat)
robot_heading = quaternion.get_yaw(root_quat)
target_position = pose_commands[:, :2]
target_heading = pose_commands[:, 2]
@@ -295,14 +295,14 @@ class AnymalCEnv(NpEnv):
)
robot_arrow_pos = robot_pos.copy()
robot_arrow_pos[:, 2] = arrow_height
robot_arrow_quat = Quaternion.from_euler(0, 0, cur_yaw)
robot_arrow_quat = quaternion.from_euler(0, 0, cur_yaw)
mocap = self._model.get_body("robot_heading_arrow").mocap
mocap.set_pose(data, np.concatenate([robot_arrow_pos, robot_arrow_quat], axis=1))
des_yaw = np.where(
np.linalg.norm(desired_vel_xy, axis=1) > 1e-6, np.arctan2(desired_vel_xy[:, 1], desired_vel_xy[:, 0]), 0.0
)
desired_arrow_quat = Quaternion.from_euler(0, 0, des_yaw)
desired_arrow_quat = quaternion.from_euler(0, 0, des_yaw)
mocap = self._model.get_body("desired_heading_arrow").mocap
mocap.set_pose(data, np.concatenate([robot_arrow_pos, desired_arrow_quat], axis=1))
@@ -350,7 +350,7 @@ class AnymalCEnv(NpEnv):
# Get robot position and heading for arrival determination
robot_position = pose[:, :2]
robot_heading = Quaternion.get_yaw(root_quat)
robot_heading = quaternion.get_yaw(root_quat)
target_position = info["pose_commands"][:, :2]
target_heading = info["pose_commands"][:, 2]
position_error = target_position - robot_position
@@ -454,7 +454,7 @@ class AnymalCEnv(NpEnv):
arrow_pos = pose_commands.copy()
arrow_pos[:, 2] = 0.05
arrow_pos = np.column_stack([pose_commands[:, 0], pose_commands[:, 1], np.full((num_envs, 1), 0.5)])
arrow_quat = Quaternion.from_euler(0, 0, pose_commands[:, 2])
arrow_quat = quaternion.from_euler(0, 0, pose_commands[:, 2])
mocap = self._model.get_body("target_marker").mocap
mocap.set_pose(data, np.concatenate([arrow_pos, arrow_quat], axis=1))
@@ -490,7 +490,7 @@ class AnymalCEnv(NpEnv):
return state.replace(terminated=terminated)
def reset(self, data: mtx.SceneData, done: np.ndarray = None) -> tuple[np.ndarray, dict]:
def reset(self, data: mtx.SceneData) -> tuple[np.ndarray, dict]:
cfg: AnymalCEnvCfg = self._cfg
num_envs = data.shape[0]
@@ -563,7 +563,7 @@ class AnymalCEnv(NpEnv):
# Calculate velocity commands (consistent with update_state)
robot_position = root_pos[:, :2]
robot_heading = Quaternion.get_yaw(root_quat)
robot_heading = quaternion.get_yaw(root_quat)
target_position = pose_commands[:, :2]
target_heading = pose_commands[:, 2]
@@ -651,4 +651,4 @@ class AnymalCEnv(NpEnv):
def _compute_projected_gravity(self, quat: np.ndarray) -> np.ndarray:
gravity = np.array([0.0, 0.0, -1.0], dtype=np.float32)
return Quaternion.rotate_vector(quat, gravity)
return quaternion.rotate_vector(quat, gravity)

View File

@@ -19,7 +19,7 @@ import numpy as np
from motrix_envs import registry
from motrix_envs.locomotion.go1.cfg import Go1WalkNpEnvCfg
from motrix_envs.math.quaternion import Quaternion
from motrix_envs.math import quaternion
from motrix_envs.np.env import NpEnv, NpEnvState
@@ -190,7 +190,7 @@ class Go1WalkTask(NpEnv):
gyro = self.get_gyro(data)
pose = self._body.get_pose(data)
base_quat = pose[:, 3:7]
local_gravity = Quaternion.rotate_inverse(base_quat, self.gravity_vec)
local_gravity = quaternion.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
@@ -320,7 +320,7 @@ class Go1WalkTask(NpEnv):
# Penalize non flat base orientation
pose = self._body.get_pose(data)
base_quat = pose[:, 3:7]
gravity = Quaternion.rotate_inverse(base_quat, self.gravity_vec)
gravity = quaternion.rotate_inverse(base_quat, self.gravity_vec)
return np.sum(np.square(gravity[:, :2]), axis=1)
def _reward_torques(self, data: mtx.SceneData):

View File

@@ -19,7 +19,7 @@ import numpy as np
from motrix_envs import registry
from motrix_envs.locomotion.go1.cfg import Go1WalkNpRoughEnvCfg
from motrix_envs.math.quaternion import Quaternion
from motrix_envs.math import quaternion
from motrix_envs.np.env import NpEnv, NpEnvState
from .common import generate_repeating_array
@@ -44,18 +44,15 @@ class Go1WalkRoughTask(NpEnv):
(self._num_dof_vel,),
dtype=np.float32,
)
self.height_list = np.array([-2.5, 0.5, 2.0])
offset_h = [[2, 2, 1, 1, 1], [2, 2, 1, 1, 2], [2, 1, 1, 1, 1], [2, 1, 1, 1, 1], [2, 1, 1, 1, 1]]
offset = []
for i in range(5):
for j in range(5):
h_index = offset_h[j][i]
offset.append([(i - 2) * 8.0, (j - 2) * 8.0, self.height_list[h_index]])
self.offset_list = np.array(offset)
go1_init_height = 0.3
self.reset_offset = self._generate_init_offsets(go1_init_height)
self._init_dof_pos = self._model.compute_init_dof_pos()
self._init_dof_pos[2] = self.height_list[0]
geom_floor_pos = self._model.get_geom("floor").local_pose[:3]
geom_floor_pos[2] += go1_init_height
self._init_dof_pos[:3] = geom_floor_pos
self._init_buffer()
self.height_counter = 0
self.reset_counter = 0
def _init_obs_space(self):
model = self.model
@@ -216,7 +213,7 @@ class Go1WalkRoughTask(NpEnv):
gyro = self.get_gyro(data)
pose = self._body.get_pose(data)
base_quat = pose[:, 3:7]
local_gravity = Quaternion.rotate_inverse(base_quat, self.gravity_vec)
local_gravity = quaternion.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
@@ -309,9 +306,9 @@ class Go1WalkRoughTask(NpEnv):
if self.training_level == 1:
num_period = 25
idx = generate_repeating_array(num_period, num_reset, self.height_counter)
self.height_counter = (self.height_counter + num_reset) % num_period
dof_pos[:, :3] = self.offset_list[idx]
idx = generate_repeating_array(num_period, num_reset, self.reset_counter)
self.reset_counter = (self.reset_counter + num_reset) % num_period
dof_pos[:, :3] = self.reset_offset[idx]
data.reset(self._model)
data.set_dof_vel(dof_vel)
@@ -364,7 +361,7 @@ class Go1WalkRoughTask(NpEnv):
# Penalize non flat base orientation
pose = self._body.get_pose(data)
base_quat = pose[:, 3:7]
gravity = Quaternion.rotate_inverse(base_quat, self.gravity_vec)
gravity = quaternion.rotate_inverse(base_quat, self.gravity_vec)
return np.sum(np.square(gravity[:, :2]), axis=1)
def _reward_torques(self, data: mtx.SceneData):
@@ -433,5 +430,48 @@ class Go1WalkRoughTask(NpEnv):
# check whether the robot reaching into the terrain border and change the move direction
border_size = 19.0
position = self._body.get_position(data)
is_out = (np.square(position[:, :2]) > border_size**2).any(axis=1)
geom_floor = self._model.get_geom("floor")
geom_floor_rough = self._model.get_geom("floor_rough")
in_rough_aera = self._is_in_area(position, border_size, border_size, geom_floor_rough.local_pose[:2])
in_flat_aera = self._is_in_area(position, border_size, border_size, geom_floor.local_pose[:2])
is_out = ~(in_rough_aera | in_flat_aera)
info["commands"][is_out] = [0, 0, 0]
def _is_in_area(self, pos, length, width, offset):
x = pos[:, 0]
y = pos[:, 1]
return (
((offset[0] - length) < x)
& ((offset[0] + length) > x)
& ((offset[1] - width) < y)
& ((offset[1] + width) > y)
)
def _generate_init_offsets(self, init_height: float, grid_size: tuple[int, int] = (5, 5)) -> np.ndarray:
"""Generate initialization offsets for rough terrain training."""
hfield = self._model.get_geom("floor_rough").hfield
nx, ny = grid_size
# Vectorized grid generation
idx_x, idx_y = np.meshgrid(np.arange(nx), np.arange(ny, dtype=np.float32), indexing="ij")
idx_x, idx_y = idx_x.flatten(), idx_y.flatten()
# Map to heightfield indices and get heights
hfield_idx_x = (hfield.ncol * idx_x // nx).astype(int)
hfield_idx_y = (hfield.nrow * idx_y // ny).astype(int)
heights = np.array([hfield.get(iy, ix) for ix, iy in zip(hfield_idx_x, hfield_idx_y)])
# Calculate positions
grid_len_x = (hfield.bound[3] - hfield.bound[0]) / nx
grid_len_y = (hfield.bound[4] - hfield.bound[1]) / ny
center_offset = np.array([grid_len_x / 2, grid_len_y / 2])
offsets = np.column_stack(
[
hfield.bound[0] + grid_len_x * idx_x + center_offset[0],
hfield.bound[4] - grid_len_y * idx_y - center_offset[1],
heights + init_height,
]
).astype(np.float32)
return offsets

View File

@@ -19,7 +19,7 @@ import numpy as np
from motrix_envs import registry
from motrix_envs.locomotion.go1.cfg import Go1WalkNpStairsEnvCfg
from motrix_envs.math.quaternion import Quaternion
from motrix_envs.math import quaternion
from motrix_envs.np.env import NpEnv, NpEnvState
from .common import generate_repeating_array
@@ -215,7 +215,7 @@ class Go1WalkStairsTask(NpEnv):
gyro = self.get_gyro(data)
pose = self._body.get_pose(data)
base_quat = pose[:, 3:7]
local_gravity = Quaternion.rotate_inverse(base_quat, self.gravity_vec)
local_gravity = quaternion.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
@@ -277,7 +277,7 @@ class Go1WalkStairsTask(NpEnv):
force = []
for foot in self.cfg.sensor.feet:
contact_force = self._model.get_sensor_value(foot + "_foot_contact", data)
contact_force = Quaternion.rotate_inverse(base_quat, contact_force)
contact_force = quaternion.rotate_inverse(base_quat, contact_force)
force.append(contact_force)
return np.concatenate(force, axis=1)
@@ -370,7 +370,7 @@ class Go1WalkStairsTask(NpEnv):
# Penalize non flat base orientation
pose = self._body.get_pose(data)
base_quat = pose[:, 3:7]
gravity = Quaternion.rotate_inverse(base_quat, self.gravity_vec)
gravity = quaternion.rotate_inverse(base_quat, self.gravity_vec)
return np.sum(np.square(gravity[:, :2]), axis=1)
def _reward_torques(self, data: mtx.SceneData):

View File

@@ -14,13 +14,14 @@
<asset>
<hfield name="hfield" file="assets/heightmap.png" size="20 20 2.5 0.1" />
<hfield name="hfield-plane" nrow="2" ncol="2" elevation="0 0 0 0" size="20 20 1.0 0.1" />
</asset>
<worldbody>
<light pos="0 0 1.5" dir="0 0 -1" directional="true" />
<geom name="floor" pos="0 0 -3" size="0 0 0.01" type="plane" material="motphys-ground"
contype="1" conaffinity="0" priority="1" friction="0.6" condim="3" />
<geom name="floor_rough" pos="0 0 -1" type="hfield" hfield="hfield" material="motphys-ground"
<geom name="floor_rough" pos="0 0 0" type="hfield" hfield="hfield" material="motphys-ground"
contype="1" conaffinity="0" priority="1" friction="0.6" />
<geom name="floor" pos="0 40 0" type="hfield" hfield="hfield-plane" material="motphys-ground"
contype="1" conaffinity="0" priority="1" friction="0.6" />
</worldbody>
</mujoco>

View File

@@ -82,8 +82,13 @@ class Asset:
foot_name = "foot"
penalize_contacts_on = ["thigh", "calf"]
terminate_after_contacts_on = [
"base_collision_0", "base_collision_1", "base_collision_2",
"fl_hip_0", "fr_hip_0", "rl_hip_0", "rr_hip_0",
"base_collision_0",
"base_collision_1",
"base_collision_2",
"fl_hip_0",
"fr_hip_0",
"rl_hip_0",
"rr_hip_0",
]
ground = "floor"

View File

@@ -19,41 +19,10 @@ import numpy as np
from motrix_envs import registry
from motrix_envs.locomotion.go2.cfg import Go2WalkNpEnvCfg
from motrix_envs.math import quaternion
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("go2-flat-terrain-walk", sim_backend="np")
class Go2WalkTask(NpEnv):
_init_dof_pos: np.ndarray
@@ -141,8 +110,6 @@ class Go2WalkTask(NpEnv):
self.hip_indices.append(i)
if "calf" in self._model.actuator_names[i]:
self.calf_indices.append(i)
print("Default joint angles:", self.default_angles)
print("Actuator names:", self._model.actuator_names)
self._init_dof_pos[-self._num_action :] = self.default_angles
@@ -213,7 +180,7 @@ class Go2WalkTask(NpEnv):
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)
local_gravity = quaternion.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
@@ -343,7 +310,7 @@ class Go2WalkTask(NpEnv):
# 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)
gravity = quaternion.rotate_inverse(base_quat, self.gravity_vec)
return np.sum(np.square(gravity[:, :2]), axis=1)
def _reward_torques(self, data: mtx.SceneData):

View File

@@ -13,4 +13,4 @@
# limitations under the License.
# ==============================================================================
from . import franka_lift_cube, franka_open_cabinet # noqa: F401 import to register envs
from . import franka_lift_cube, franka_open_cabinet, shadow_hand # noqa: F401 import to register envs

View File

@@ -240,11 +240,11 @@ class FrankaLiftCubeEnv(NpEnv):
## action penalty rate
reach_weight = 1.5 # Cannot be too small
cmd_tracking_weight = 10
cmd_tracking_fine_graind_weight = 20 # Should be larger, need strong pull to target area
object_command_tracking_close_reward_weight = 10
cmd_tracking_weight = 10.0
cmd_tracking_fine_graind_weight = 20.0 # Should be larger, need strong pull to target area
object_command_tracking_close_reward_weight = 10.0
if self.count < 10000:
if self.count < 20000:
action_penalty_rate = 1e-4
joint_vel_penalty_rate = 1e-4
else:

View File

@@ -18,7 +18,7 @@ import motrixsim as mtx
import numpy as np
from motrix_envs import registry
from motrix_envs.math.quaternion import Quaternion
from motrix_envs.math import quaternion
from motrix_envs.np.env import NpEnv, NpEnvState
from .cfg import FrankaOpenCabinetEnvCfg
@@ -206,7 +206,7 @@ class FrankaOpenCabinetEnv(NpEnv):
dist_reward *= 10
## matching orientation reward
quat_reward = Quaternion.similarity(robot_grasp_pose[:, -4:], drawer_grasp_pose[:, -4:])
quat_reward = quaternion.similarity(robot_grasp_pose[:, -4:], drawer_grasp_pose[:, -4:])
## close gripper reward
# When gripper distance < 0.025, closing gripper gets reward
@@ -227,6 +227,7 @@ class FrankaOpenCabinetEnv(NpEnv):
open_reward = (
np.bitwise_not(wrong_open) * open_reward
) # No reward for forced opening (can't force open after increasing MJCF resistance)
quat_reward = np.where(open_reward > 0, 1.0, quat_reward)
##################### Penalty Terms #####################"
## Action penalty

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.
# ==============================================================================
from .cfg import ShadowHandReposeEnvCfg
# Use the full implementation from env.py (157-dim obs with fingertips)
from .shadow_hand_np import ShadowHandReposeEnv
__all__ = ["ShadowHandReposeEnvCfg", "ShadowHandReposeEnv"]

View File

@@ -0,0 +1,142 @@
# 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 for Shadow Hand Cube Reorientation Environment"""
import os
from dataclasses import dataclass
from typing import List, Tuple
from motrix_envs import registry
from motrix_envs.base import EnvCfg
# Path to the repose_cube.xml model
model_file = os.path.join(os.path.dirname(__file__), "xmls", "repose_cube.xml")
@registry.envcfg("shadow-hand-repose")
@dataclass
class ShadowHandReposeEnvCfg(EnvCfg):
"""
Configuration for Shadow Hand Cube Reorientation Environment
This environment trains the Shadow Hand to reorient a cube to match
randomly sampled target orientations.
"""
# ====================
# Model Configuration
# ====================
model_file: str = model_file
# ====================
# Simulation Parameters
# ====================
sim_dt: float = 0.01
sim_substeps: int = 1 # Number of simulation steps per control step
ctrl_dt: float = sim_dt * sim_substeps
max_episode_seconds: float = 10.0
max_episode_steps: int = int(max_episode_seconds / ctrl_dt)
# ====================
# Robot Configuration
# ====================
num_hand_dofs: int = 24 # Total DOFs in Shadow Hand
num_actuators: int = 20 # Actuated joints
# Fingertip link names for forward kinematics
fingertip_link_names: List[str] = (
"rh_ffdistal", # First finger (index) distal
"rh_mfdistal", # Middle finger distal
"rh_rfdistal", # Ring finger distal
"rh_lfdistal", # Little finger distal
"rh_thdistal", # Thumb distal
)
# ====================
# Object Configuration
# ====================
cube_initial_pos: Tuple[float, float, float] = (0.33, 0.00, 0.295) # Initial cube position
# ====================
# Reward Parameters
# ====================
# Core reward components
dist_reward_scale: float = -10.0 # Balanced for MotrixSim (推荐)
rot_reward_scale: float = 1.0 # Moderate rotation reward
rot_eps: float = 0.1 # Stable denominator
action_penalty_scale: float = -0.0002
# Success and failure criteria
success_tolerance: float = 0.1 # ~8.6° (moderate challenge)
reach_goal_bonus: float = 2.0 # Balanced incentive
fall_dist: float = 0.24 # Reasonable manipulation space # Distance threshold for dropping cube (meters)
fall_penalty: float = 0.0 # Penalty for dropping the cube
# In-hand distance threshold (only used for success check, not reward)
in_hand_dist_threshold: float = 0.05 # Distance threshold for "in-hand" (5cm)
# Success hold mechanism (uses max_consecutive_successes)
max_consecutive_successes: int = 50 # Reset after holding success for this many steps
# Averaging factor for consecutive successes tracking
av_factor: float = 0.1
# ====================
# Reset Noise Parameters
# ====================
reset_position_noise: float = 0.01 # Increased robustness
reset_dof_pos_noise: float = 0.2 # Higher generalization
reset_dof_vel_noise: float = 0.0 # DOF velocity noise at reset
# ====================
# Observation Scaling
# ====================
vel_obs_scale: float = 0.2 # Scale factor for velocity observations
# ====================
# Action Processing
# ====================
act_moving_average: float = 1.0 # Action smoothing (1.0 = no smoothing)
# ====================
# Visualization
# ====================
# Offset for target visualization (relative to hand position)
# Recommended: offset to upper-left to avoid occluding the real hand and cube
viz_target_offset: Tuple[float, float, float] = (
0.0, # Left (negative X)
0.0, # Forward/Up (negative Y)
0.2, # Up (positive Z)
)
# ====================
# Domain Randomization (Optional)
# ====================
# Enable domain randomization (recommended to start with False)
enable_domain_randomization: bool = False
# Randomization parameters (only used if enable_domain_randomization=True)
randomize_friction: bool = False
friction_range: Tuple[float, float] = (0.8, 12)
randomize_mass: bool = False
mass_range: Tuple[float, float] = (0.8, 1.2)
randomize_com: bool = False
com_displacement_range: float = 0.01

View File

@@ -0,0 +1,391 @@
# 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.
# ==============================================================================
"""
Shadow Hand Cube Reorientation Environment for MotrixSim
This environment implements the classic in-hand cube manipulation task where the
Shadow Hand must reorient a cube to match random target orientations.
"""
import gymnasium as gym
import motrixsim as mtx
import numpy as np
from motrix_envs import registry
from motrix_envs.math import quaternion, utils
from motrix_envs.np.env import NpEnv, NpEnvState
from .cfg import ShadowHandReposeEnvCfg
@registry.env("shadow-hand-repose", sim_backend="np")
class ShadowHandReposeEnv(NpEnv):
"""
Shadow Hand Cube Reorientation Environment
Observation space: 157 dimensions
- 24: hand dof positions (unscaled)
- 24: hand dof velocities (scaled by 0.2)
- 7: object pose (pos + quat)
- 3: object linear velocity
- 3: object angular velocity (scaled by 0.2)
- 7: goal pose (pos + quat)
- 4: relative quaternion (object to goal)
- 65: fingertip states (5 fingertips * 13: pos + quat + vel)
- 20: previous actions
Action space: 20 dimensions (normalized [-1, 1] position targets for actuators)
"""
_cfg: ShadowHandReposeEnvCfg
def __init__(self, cfg: ShadowHandReposeEnvCfg, num_envs: int = 1):
super().__init__(cfg, num_envs=num_envs)
# Get model info
self._num_hand_dofs = cfg.num_hand_dofs # 24 total DOFs
self._num_actuators = cfg.num_actuators # 20 actuated joints
# Initialize spaces
self._action_space = gym.spaces.Box(low=-1.0, high=1.0, shape=(self._num_actuators,), dtype=np.float32)
self._observation_space = gym.spaces.Box(low=-np.inf, high=np.inf, shape=(157,), dtype=np.float32)
# Get actuator control ranges from model
self._actuator_ctrl_lower = self._model.actuator_ctrl_limits[0, :]
self._actuator_ctrl_upper = self._model.actuator_ctrl_limits[1, :]
# Get joint limits for all DOFs (use model's joint_limits directly)
self._hand_dof_lower_limits = self._model.joint_limits[0, :]
self._hand_dof_upper_limits = self._model.joint_limits[1, :]
# Fingertip link indices (use get_link_index for pose/velocity access)
self._fingertip_link_ids = []
for name in cfg.fingertip_link_names:
link_id = self._model.get_link_index(name)
self._fingertip_link_ids.append(link_id)
self._num_fingertips = len(self._fingertip_link_ids)
# Get cube and target link indices
self._cube_link_id = self._model.get_link_index("cube")
self._cube_body = self._model.get_body("cube")
self._cube_dof_vel_indices = self._cube_body.get_dof_vel_indices()
# Target is a mocap body - access via Body object (no get_mocap_index API)
self._target_body = self._model.get_body("target")
self._target_link_id = self._model.get_link_index("target")
assert self._target_body.is_mocap, "Target must be a mocap body"
# Initial cube position (in hand)
self._in_hand_pos = np.array(cfg.cube_initial_pos, dtype=np.float32)
@property
def observation_space(self):
return self._observation_space
@property
def action_space(self):
return self._action_space
def _extract_cube_states(self, data: mtx.SceneData, body: mtx.Body):
return body.get_position(data), body.get_rotation(data), data.dof_vel[:, body.get_dof_vel_indices()]
def _extract_link_states(self, data: mtx.SceneData, link_ids):
"""
Extract position, quaternion, and velocity for specified links.
Args:
data: SceneData object
link_ids: List of link indices or single int
Returns:
Tuple of (positions, quaternions, velocities)
- positions: (batch, num_links, 3)
- quaternions: (batch, num_links, 4) in (x, y, z, w) format
- velocities: (batch, num_links, 6) [linear_vel, angular_vel]
"""
# Ensure link_ids is a list
if isinstance(link_ids, int):
link_ids = [link_ids]
# Get all link poses: shape (batch, num_links_total, 7) [x, y, z, qx, qy, qz, qw]
all_poses = self._model.get_link_poses(data)
# Extract poses for requested links
poses = all_poses[:, link_ids, :] # (batch, num_requested_links, 7)
# Split into position and quaternion
positions = poses[:, :, :3]
quaternions = poses[:, :, 3:] # (qx, qy, qz, qw)
# TODO: Velocity computation - MotrixSim doesn't expose link velocities yet
# Temporary solution: use zero velocities
# Future options:
# 1. Finite differences (requires storing previous poses)
# 2. Compute from DOF velocities via Jacobian (if available)
# 3. Wait for API: model.get_link_velocities(data)
batch_size = data.shape[0]
num_links = len(link_ids)
velocities = np.zeros((batch_size, num_links, 6), dtype=np.float32)
for j in np.arange(num_links):
link = self._model.get_link(link_ids[j])
velocities[:, j] = np.concatenate(
(link.get_linear_velocity(data), link.get_angular_velocity(data)), axis=-1
)
return positions, quaternions, velocities
def apply_action(self, actions: np.ndarray, state: NpEnvState):
"""Apply actions to the hand actuators."""
cfg = self._cfg
# Scale actions from [-1, 1] to actuator control range
targets = utils.scale(actions, self._actuator_ctrl_lower, self._actuator_ctrl_upper)
# Apply action moving average for smoothness
if cfg.act_moving_average < 1.0:
targets = cfg.act_moving_average * targets + (1.0 - cfg.act_moving_average) * state.info["prev_actions"]
# Clamp to control limits
targets = np.clip(targets, self._actuator_ctrl_lower, self._actuator_ctrl_upper)
# Set actuator controls
state.data.actuator_ctrls = targets
state.info["prev_actions"] = targets.copy()
return state
def update_state(self, state: NpEnvState):
"""Update observations, rewards, and termination conditions."""
data = state.data
info = state.info
# compute obs
obs = self._compute_observation(state.data, info)
# Compute reward and termination
reward, terminated, goal_reached = self._compute_reward(state, info)
if np.any(goal_reached):
reset_goal_indices = np.where(goal_reached)[0]
self._reset_goal_pose(info, reset_goal_indices)
# Update target visualization
self._update_target_visualization(data, info)
state.obs = obs
state.reward = reward
state.terminated = terminated
return state
def _compute_observation(self, data: mtx.SceneData, info: dict):
cfg = self._cfg
num_envs = data.shape[0]
# Get hand DOF states
hand_dof_pos = data.dof_pos[:, : self._num_hand_dofs]
hand_dof_vel = data.dof_vel[:, : self._num_hand_dofs]
# Get cube state using link poses
cube_pos, cube_quat, cube_vel = self._extract_cube_states(data, self._cube_body)
cube_linvel = cube_vel[:, :3]
cube_angvel = cube_vel[:, 3:]
# Get fingertip states using link poses
fingertip_pos, fingertip_quat, fingertip_vel = self._extract_link_states(data, self._fingertip_link_ids)
# Flatten fingertip states (5 × 13 = 65)
fingertip_state = np.concatenate(
[
fingertip_pos.reshape(num_envs, -1), # 15
fingertip_quat.reshape(num_envs, -1), # 20
fingertip_vel.reshape(num_envs, -1), # 30
],
axis=-1,
) # Total: 65
# Compute relative quaternion
relative_quat = quaternion.mul(cube_quat, quaternion.conjugate(info["goal_rot"]))
scaled_hand_pos = utils.unscale(hand_dof_pos, self._hand_dof_lower_limits, self._hand_dof_upper_limits)
# Build observation (157 dims)
return np.concatenate(
[
scaled_hand_pos,
cfg.vel_obs_scale * hand_dof_vel, # 24
cube_pos, # 3
cube_quat, # 4
cube_linvel, # 3
cfg.vel_obs_scale * cube_angvel, # 3
info["goal_pos"], # 3
info["goal_rot"], # 4
relative_quat, # 4
fingertip_state, # 65
info["prev_actions"], # 20
],
axis=-1,
)
def _compute_reward(self, state: NpEnvState, info: dict):
"""
Reward components (3 core items):
1. Position distance penalty
2. Rotation alignment reward
3. Action regularization penalty
Additional rewards/penalties:
- Success bonus when goal is reached
- Fall penalty when cube drops
- Timeout penalty when episode ends without success
"""
cfg = self._cfg
num_envs = self._num_envs
# Get cube state using link poses
cube_pos, cube_quat, _ = self._extract_cube_states(state.data, self._cube_body)
# Distance from cube to goal position
goal_dist = np.linalg.norm(cube_pos - state.info["goal_pos"], axis=-1)
# Rotation distance
rot_dist = quaternion.rotation_distance(cube_quat, state.info["goal_rot"])
# Core reward components
dist_rew = goal_dist * cfg.dist_reward_scale
rot_rew = 1.0 / (np.abs(rot_dist) + cfg.rot_eps) * cfg.rot_reward_scale
action_penalty = np.sum(state.info["prev_actions"] ** 2, axis=-1) * cfg.action_penalty_scale
# Base reward
reward = dist_rew + rot_rew + action_penalty
# Check for success (only rotation tolerance)
goal_reached = np.abs(rot_dist) <= cfg.success_tolerance
# Update success counter
info["successes"] += goal_reached * 1
# Success bonus
reward = np.where(goal_reached, reward + cfg.reach_goal_bonus, reward)
# Fall penalty
fallen = goal_dist >= cfg.fall_dist
reward = np.where(fallen, reward + cfg.fall_penalty, reward)
# Termination conditions
terminated = np.zeros(num_envs, dtype=bool)
# 1. Fall termination
terminated = np.logical_or(terminated, fallen)
# 2. Success termination with hold mechanism
if cfg.max_consecutive_successes > 0:
# Reset progress on goal reached when max consecutive successes reached
new_pos = info["successes"] >= cfg.max_consecutive_successes
info["successes"] *= 1 - new_pos
# 3. NaN protection
terminated = np.logical_or(terminated, np.isnan(rot_dist))
terminated = np.logical_or(terminated, np.isnan(goal_dist))
return reward, terminated, new_pos
def _update_target_visualization(self, data: mtx.SceneData, info: dict):
"""Update the target mocap body to visualize the goal pose."""
cfg = self._cfg
# Compute visualization position (offset from goal position)
viz_pos = info["goal_pos"] + np.array(cfg.viz_target_offset, dtype=np.float32)
# Combine into pose array: [x, y, z, qx, qy, qz, qw]
viz_pose = np.concatenate([viz_pos, info["goal_rot"]], axis=-1)
# Update mocap body pose using correct API
self._target_body.mocap.set_pose(data, viz_pose)
def reset(self, data: mtx.SceneData):
"""Reset environments."""
cfg = self._cfg
# data is already filtered to contain only envs that need reset
num_resets = data.shape[0]
# Reset scene data
data.reset(self._model)
# Reset hand DOFs with noise
init_dof_pos = self._model.compute_init_dof_pos()
init_dof_vel = np.zeros(self._model.num_dof_vel, dtype=np.float32)
# Add noise to DOF positions
dof_pos_noise = np.random.uniform(
-cfg.reset_dof_pos_noise,
cfg.reset_dof_pos_noise,
(num_resets, self._num_hand_dofs),
)
# Add noise to DOF velocities
dof_vel_noise = np.random.uniform(
-cfg.reset_dof_vel_noise, cfg.reset_dof_vel_noise, (num_resets, self._num_hand_dofs)
)
# Set DOF states for all envs in data (already filtered)
dof_pos = np.tile(init_dof_pos, (num_resets, 1))
dof_vel = np.tile(init_dof_vel, (num_resets, 1))
dof_pos[:, : self._num_hand_dofs] += dof_pos_noise
dof_vel[:, : self._num_hand_dofs] += dof_vel_noise
data.set_dof_pos(dof_pos, self._model)
data.set_dof_vel(dof_vel)
# Reset cube position with small noise
cube_pos_noise = np.random.uniform(-cfg.reset_position_noise, cfg.reset_position_noise, (num_resets, 3))
cube_pos = np.tile(self._in_hand_pos, (num_resets, 1))
cube_pos += cube_pos_noise
# Randomize cube orientation
cube_quat = quaternion.generate_random_shoemake(num_resets)
# Set cube pose using body's set_dof_pos method
# Combine into DOF pose: [x, y, z, qx, qy, qz, qw]
cube_dof_pos = np.concatenate([cube_pos, cube_quat], axis=-1)
# Set cube DOF position
self._cube_body.set_dof_pos(data, cube_dof_pos)
# Set cube DOF velocity to zero
cube_dof_vel = np.zeros((num_resets, 6), dtype=np.float32) # 6DOF velocity
self._cube_body.set_dof_vel(data, cube_dof_vel)
# Reset goal pose
# Note: goal_pos and goal_rot are indexed by original env indices
info = {
"goal_pos": np.tile(self._in_hand_pos, num_resets).reshape(num_resets, 3),
"goal_rot": quaternion.generate_random_shoemake(num_resets),
"prev_actions": np.zeros((num_resets, self._num_actuators), dtype=np.float32),
"successes": np.zeros((num_resets), dtype=np.int32),
}
obs = self._compute_observation(data, info)
return obs, info
def _reset_goal_pose(self, info, env_ids):
"""Reset goal pose to random orientation with fixed position."""
num_resets = len(env_ids)
# Goal position is fixed
# Randomize goal orientation using Shoemake method for uniform SO(3) sampling
info["goal_rot"][env_ids] = quaternion.generate_random_shoemake(num_resets)

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 2022 Shadow Robot Company Ltd
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.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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