# Action Space — 29 DoF Humanoid

The Unitree G1 has 29 controllable joints organized into 6 groups. Neon's action space defines every joint with its physical limits, default pose, and normalization scheme.

---

## Joint Layout

```
            ┌───────────┐
            │   Head     │  2 DoF (pitch, yaw)
            │  [15-16]   │
            └─────┬─────┘
                  │
            ┌─────┴─────┐
   Left Arm │   Torso    │ Right Arm
   7 DoF    │   1 DoF    │ 7 DoF
   [0-6]    │   [14]     │ [7-13]
            └─────┬─────┘
                  │
         ┌────────┴────────┐
   Left Leg              Right Leg
   6 DoF                 6 DoF
   [17-22]               [23-28]
```

## Control Modes

Neon supports three control modes, letting you start simple and scale up:

| Mode | Joints | Action Dim | Use Case |
|------|--------|-----------|----------|
| `arms_only` | 14 (7+7 arms including grippers) | 17 (14 + 3 loco) | Tabletop manipulation |
| `upper_body` | 17 (arms + torso + head) | 20 (17 + 3 loco) | Upper body tasks |
| `whole_body` | 29 (all joints) | 32 (29 + 3 loco) | Full locomotion + manipulation |

The extra 3 dimensions are **locomotion velocity**: `(vx, vy, omega)` — forward speed, lateral speed, and rotation rate.

```python
from neon.data.action_space import G1ActionSpace, ControlMode

# Start with arms only
space = G1ActionSpace(mode=ControlMode.ARMS_ONLY, include_locomotion=True)
print(space.action_dim)   # 17
print(space.joint_names)  # ['left_shoulder_pitch', 'left_shoulder_roll', ...]

# Scale to whole body later
space = G1ActionSpace(mode=ControlMode.WHOLE_BODY, include_locomotion=True)
print(space.action_dim)   # 32
```

---

## Joint Details

### Arms (14 DoF)

Each arm has 7 joints: 3 shoulder, 1 elbow, 2 wrist, 1 gripper.

| Joint | Index | Range (rad) | Description |
|-------|-------|-------------|-------------|
| `left_shoulder_pitch` | 0 | [-2.87, 2.87] | Forward/backward swing |
| `left_shoulder_roll` | 1 | [-0.34, 3.11] | Outward/inward rotation |
| `left_shoulder_yaw` | 2 | [-1.30, 2.18] | Internal/external rotation |
| `left_elbow` | 3 | [-1.25, 2.61] | Bend |
| `left_wrist_yaw` | 4 | [-1.57, 1.57] | Wrist twist |
| `left_wrist_roll` | 5 | [-0.52, 0.52] | Wrist tilt |
| `left_gripper` | 6 | [0.00, 1.00] | Open (0) / Close (1) |

The right arm mirrors the left with index offset +7 and symmetric limits.

### Legs (12 DoF)

Each leg has 6 joints: 2 hip (roll, yaw), 1 hip pitch, 1 knee, 2 ankle.

| Joint | Index | Range (rad) | Description |
|-------|-------|-------------|-------------|
| `left_hip_roll` | 17 | [-0.52, 0.52] | Side-to-side |
| `left_hip_yaw` | 18 | [-0.52, 0.52] | Rotation |
| `left_hip_pitch` | 19 | [-1.57, 0.52] | Forward/backward |
| `left_knee` | 20 | [0.00, 2.53] | Bend (always positive) |
| `left_ankle_pitch` | 21 | [-0.87, 0.52] | Foot tilt |
| `left_ankle_roll` | 22 | [-0.17, 0.17] | Foot roll (narrow range) |

---

## Normalization

All actions are normalized to `[-1, 1]` for training stability. The mapping uses each joint's physical limits:

$$
a_{normalized} = \frac{a_{raw} - \frac{lo + hi}{2}}{\frac{hi - lo}{2}}
$$

```python
import numpy as np

space = G1ActionSpace(mode=ControlMode.ARMS_ONLY)

# Raw joint angles (radians)
raw = np.array([0.5, 1.0, -0.3, ...])  # 14 joints

# Normalize to [-1, 1]
normalized = space.normalize(raw)

# Denormalize back to radians
recovered = space.denormalize(normalized)
np.testing.assert_allclose(raw, recovered, atol=1e-6)  # ✓ Round-trip
```

!!! warning "Locomotion normalization"
    Locomotion velocity uses different limits: `vx ∈ [-1.5, 1.5] m/s`, `vy ∈ [-0.5, 0.5] m/s`, `omega ∈ [-1.0, 1.0] rad/s`. These are configurable via `locomotion_limits`.

---

## Group-Based Access

Split a full action vector into named groups for inspection or per-group control:

```python
actions = np.random.randn(16, 17)  # 16 timesteps, 17 DoF

split = space.split_action(actions)

split["left_arm"]     # (16, 7) — shoulder + elbow + wrist + gripper
split["right_arm"]    # (16, 7)
split["locomotion"]   # (16, 3) — vx, vy, omega
```

This is used internally by the [separate action heads](../guide/action-heads.md) — each group gets its own MLP decoder.

---

## Cross-Embodiment Mapping

The `ActionMapper` maps actions from other robots to the G1 action space:

```python
from neon.data.data_soup import ActionMapper

mapper = ActionMapper(target_space=space)

# Franka actions (7 DoF + gripper) → G1 right arm
franka_actions = np.random.randn(100, 8)
g1_actions = mapper.map_actions(franka_actions, "franka")
print(g1_actions.shape)  # (100, 17) — mapped to G1 space
```

Supported source embodiments: `g1`, `agibot`, `franka`, `so100`, `gr1`.
