# MicroDuck 61-Dimensional Observation Space Structure Explained

> Understand the 61-dimensional observation space structure in MicroDuck RL. Learn how it combines angular velocity, gravity, joint states, and actions for policy portability across tasks.

- Repository: [Pollen Robotics/microduck_rl](https://github.com/pollen-robotics/microduck_rl)
- Tags: deep-dive
- Published: 2026-09-02

---

**The 61-dimensional observation space in MicroDuck RL environments is a fixed-order concatenation of base angular velocity, projected gravity, joint positions and velocities, previous actions, and command inputs, enabling policy portability across walking, stand-up, and roller tasks.**

The `microduck_rl` repository implements a standardized observation vector that every reinforcement learning task shares. This flat tensor—referred to as the **"actor" observation**—is constructed in [`src/mjlab_microduck/tasks/symmetry.py`](https://github.com/pollen-robotics/microduck_rl/blob/main/src/mjlab_microduck/tasks/symmetry.py) and consumed by all task configurations including velocity tracking, stand-up recovery, and roller skating.

## 61-Dimensional Observation Space Breakdown

The observation vector is assembled by stacking seven sensor and command groups in a strict index order. Deviating from this layout breaks policy compatibility.

| Index Range | Component | Dimensions | Description |
|-------------|-----------|------------|-------------|
| 0 – 2 | `base_ang_vel` | 3 | Body-frame angular velocity (roll, pitch, yaw) from the IMU |
| 3 – 5 | `projected_gravity` | 3 | Gravity direction vector expressed in body coordinates |
| 6 – 19 | `joint_pos_rel` | 14 | Joint positions relative to default pose |
| 20 – 33 | `joint_vel_rel` | 14 | Joint velocities relative to default pose |
| 34 – 47 | `last_action` | 14 | Action vector applied in the previous simulation step |
| 48 – 50 | `twist command` | 3 | Desired body-frame velocity (linear x, linear y, angular z) |
| 51 – 54 | `head command` | 4 | Incremental head orientation deltas (neck pitch, head pitch, yaw, roll) |
| 55 – 60 | `body command` | 6 | Incremental whole-body pose deltas (position x/y/z, orientation roll/pitch/yaw) |

The source code explicitly documents this ordering in [`symmetry.py`](https://github.com/pollen-robotics/microduck_rl/blob/main/symmetry.py) lines 9-18:

```text
[0:3]   base_ang_vel
[3:6]   projected_gravity
[6:20]  joint_pos_rel
[20:34] joint_vel_rel
[34:48] last_action
[48:51] twist command
[51:55] head command
[55:61] body command

```

## Joint Ordering Convention

The 14 joints in `joint_pos_rel` (indices 6-19) and `joint_vel_rel` (indices 20-33) follow a consistent left-to-right pattern:

- left_hip_yaw
- left_hip_roll
- left_hip_pitch
- left_knee
- left_ankle
- neck_pitch
- head_pitch
- head_yaw
- head_roll
- right_hip_yaw
- right_hip_roll
- right_hip_pitch
- right_knee
- right_ankle

This ordering enables **symmetry augmentation**: the `microduck_vel_symmetry` utilities in [`symmetry.py`](https://github.com/pollen-robotics/microduck_rl/blob/main/symmetry.py) permute and sign-flip specific indices when mirroring left and right limbs.

## Why the Fixed Structure Matters

**Policy portability** — A model trained on one task (e.g., velocity tracking) can execute on another (e.g., roller skating) without retraining the observation encoder. The neural network input size and semantic meaning remain constant.

**Symmetry operations** — The permutation tables in [`src/mjlab_microduck/tasks/symmetry.py`](https://github.com/pollen-robotics/microduck_rl/blob/main/src/mjlab_microduck/tasks/symmetry.py) rely on predictable index positions to implement data augmentation for bilateral symmetry.

**Curriculum safety** — Reward terms in any `*_env_cfg.py` file can safely reference observation slots by index, knowing the layout is invariant across task variants.

## Reading the Observation Shape in Code

```python
import numpy as np
from mjlab_microduck.tasks.microduck_velocity_env_cfg import make_microduck_velocity_env_cfg

cfg = make_microduck_velocity_env_cfg(play=False, rough=False)
env = cfg.make()
obs = env.reset()                     # obs is a dict with "actor" and "critic"

print(obs["actor"].shape)             # → torch.Size([1, 61])

print(obs["actor"][0, :3])            # base angular velocity (roll, pitch, yaw)

print(obs["actor"][0, 6:20])          # relative joint positions

print(obs["actor"][0, 48:51])         # twist command (vx, vy, omega_z)

```

## Key Source Files

- **[`src/mjlab_microduck/tasks/symmetry.py`](https://github.com/pollen-robotics/microduck_rl/blob/main/src/mjlab_microduck/tasks/symmetry.py)** — Defines the 61-D actor observation layout and symmetry permutation/sign tables
- **[`src/mjlab_microduck/tasks/mdp.py`](https://github.com/pollen-robotics/microduck_rl/blob/main/src/mjlab_microduck/tasks/mdp.py)** — Registers observation groups (`actor`, `critic`) and constructs the flat tensors
- **`src/mjlab_microduck/tasks/*_env_cfg.py`** — Task-specific configurations referencing the shared observation layout
- **[`tests/test_spin_cfg.py`](https://github.com/pollen-robotics/microduck_rl/blob/main/tests/test_spin_cfg.py)** — Unit tests asserting the observation layout remains stable across task variants

## Summary

- The **61-dimensional observation space** is a concatenated vector with fixed index ranges for each sensor group
- **Base IMU data** occupies indices 0-5 (angular velocity + projected gravity)
- **Joint state** spans indices 6-33 (14 positions + 14 velocities, relative to default pose)
- **Action history** sits at indices 34-47 for policy stability
- **Command inputs** fill indices 48-60 across twist, head, and body command groups
- All task configurations in `microduck_rl` share this layout to guarantee policy interoperability

## Frequently Asked Questions

### How do I extract specific joint angles from the 61-dimensional observation?

Slice indices 6-19 for `joint_pos_rel` or 20-33 for `joint_vel_rel`. Use the documented joint ordering: left hip chain (5 joints), neck and head (4 joints), then right hip chain (5 joints). The neck_pitch at index 6 separates left and right limbs.

### Can I modify the observation space size for custom tasks?

No—this breaks the **policy portability** contract. All task configurations in `microduck_rl` enforce the 61-D structure. Add derived features through wrapper observations or modify [`symmetry.py`](https://github.com/pollen-robotics/microduck_rl/blob/main/symmetry.py) only if you retrain all policies from scratch.

### What is the difference between "actor" and "critic" observations?

The "actor" observation (61-D) is the fixed policy input described here. The "critic" observation may contain additional privileged information for value estimation, such as contact forces or terrain height maps, depending on the task configuration in [`mdp.py`](https://github.com/pollen-robotics/microduck_rl/blob/main/mdp.py).

### Why are joint positions relative to a default pose rather than absolute?

Relative positions improve **policy generalization** across different initial configurations and reduce the input distribution shift when the robot starts from varied poses. The default pose is defined in the asset configuration and subtracted before populating indices 6-19.