LEAP hand: spinning a cube (PPO, MuJoCo Playground)

A policy for the LEAP hand (16 joints, palm up) that turns a cube in its palm about the vertical axis, trained with PPO (Brax) in MuJoCo Playground's LeapCubeRotateZAxis with random friction, masses and joint properties, and exported to plain NumPy. Simulation only: not validated on a real hand.

Spinning the cube, slow motion, holding it still, and following a plain-language instruction.

Results

Tested on 128 randomized hands, 10 seconds each:

measured goal
drops the cube 2.3 % of episodes under 10 %
keeps the cube in the palm 97.9 % of the time
spin speed 0.94 rad/s (1.47 turns in 10 s) 1.0 rad/s, not reached yet

In the video: spin for 6 s turns the cube 2.79 rad; holding still (zero action) for 3 s it moves 0.28 rad; no drop.

Following plain language. "spin the cube for five seconds, then hold it still for three seconds": a language model running locally planned it in 1.4 s as spin 5 s, hold 3 s; the hand turned the cube 1.32 rad, then held it within 0.19 rad, no drop. The planner is not part of this model: it only chooses between this policy ("spin") and zero action ("hold").

Spin for five seconds, then hold it still for three seconds.

How it was trained

  • Environment: MuJoCo Playground 0.2.0 LeapCubeRotateZAxis, control every 0.05 s, joint targets = default pose + 0.6 × action.
  • Observation (32 numbers): the 16 joint angles (noisy in training) and the 16 previous actions. The cube's pose and speed were given only to the value network during training (asymmetric actor-critic).
  • Network: MLP 512-256-128, swish; output 2 × 16 (mean, scale); action = tanh(mean).
  • Training: Brax PPO, 100 M steps, 8192 parallel hands, 4.3 hours on a home server's CPU (AMD Ryzen AI MAX+ 395); Playground's LEAP domain randomizer. Evaluation reward rose from −0.58 to 27.65 and was still rising.

Use

import numpy as np

z = np.load("policy.npz")
n = int(z["n_layers"])
W, b = [z[f"w{i}"] for i in range(n)], [z[f"b{i}"] for i in range(n)]

def act(obs):
    """Deterministic action in [-1, 1] for one observation (Brax PPO inference, no JAX needed)."""
    x = (obs - z["obs_mean"]) / z["obs_std"]
    for w, bias in zip(W[:-1], b[:-1]):
        x = x @ w + bias
        x = 0.5 * x * (1 + np.tanh(0.5 * x))          # swish
    out = x @ W[-1] + b[-1]
    return np.tanh(out[: int(z["action_size"])])

targets = z["default_pose"] + z["action_scale"] * act(obs)   # joint position targets

The export keeps only the policy (the normaliser statistics of the 32-number observation and the MLP); it matches Brax's deterministic inference within 1e-5. Joint order: actuator_names in the file. Experiments behind these numbers: YauhenBichel/humanoid-lab-experiments.

Limits

Simulation only; spins slower than the goal; one skill (turning about the vertical axis). Not a medical, care or safety-rated device.

Credits: MuJoCo Playground and the LEAP hand model (Apache-2.0). Not affiliated with the LEAP hand authors or Google DeepMind.

Downloads last month

-

Downloads are not tracked for this model. How to track
Video Preview
loading