Teaching a Humanoid Robot to Wave: Custom Motions with GEAR-SONIC
24th February 2026
GEAR-SONIC can track arbitrary motions, not only its built-in locomotion styles. You do this in three stages:
- Define joint angles in a CSV.
- Preview them with direct replay in MuJoCo.
- Deploy them through the SONIC neural network.
Here’s how the three-stage pipeline works.
Stage 1: Create the Motion
The script create_motion.py lets you define joint angles for each frame at 50 Hz. The Unitree G1 has 29 joints in IsaacLab ordering:
Legs (0-11) → Waist (12-14) → Left Arm (15-21) → Right Arm (22-28)
For example, you build a wave gesture frame by frame with a smooth envelope that ramps 0→1, holds, then ramps 1→0:
for each frame:
t = frame / 50Hz
env = smooth_envelope(t) # ramps up 0→1, holds, ramps down 1→0
poses[R_SH_P] = env * -0.8 # shoulder pitch (arm slightly forward)
poses[R_SH_R] = env * -1.2 # shoulder roll (arm out to side)
poses[R_ELB] = env * 1.5 # elbow bent 90° (forearm up)
poses[R_WR_Y] = env * sin(2.5Hz) # wrist oscillates side-to-side
This outputs six CSV files:
wave/
├── joint_pos.csv (250 × 29) — joint angles per frame
├── joint_vel.csv (250 × 29) — finite-difference velocities
├── body_pos.csv (250 × 42) — 14 body positions (x,y,z)
├── body_quat.csv (250 × 56) — 14 body orientations (w,x,y,z)
├── body_lin_vel.csv (250 × 42) — body linear velocities
├── body_ang_vel.csv (250 × 42) — body angular velocities
└── metadata.txt — body part indexes, frame count
250 frames at 50 Hz gives a 5-second motion clip. The script precomputes every quantity the SONIC encoder needs and writes it to disk:
- joint positions
- velocities
- body orientations
- angular velocities
Stage 2: Visualize the Motion
Before involving any neural network, you verify the motion with direct replay:
python visualize_motion.py --motion_dir wave/
This script does four things:
- Reads
joint_pos.csv. - Reorders the joints from IsaacLab ordering to MuJoCo ordering.
- Sets
qposdirectly. - Renders.
It uses no policy and no controller. It only applies raw joint angles frame by frame.
The reordering is necessary because IsaacLab and MuJoCo number joints differently:
isaaclab_to_mujoco = [0,3,6,9,13,17, 1,4,7,10,14,18, 2,5,8,11,15,19, ...]
What you see here is the ground truth: exactly the motion you defined. If it looks wrong at this stage, fix the CSV before you continue.
Stage 3: Deploy Through SONIC
This is the full pipeline with the neural network. It runs in two terminals:
# Terminal 1: MuJoCo physics simulation
python gear_sonic/scripts/run_sim_loop.py
# Terminal 2: SONIC C++ inference with your motion data
bash deploy.sh --motion-data wave/ sim
Here’s what happens inside the SONIC C++ binary every 20ms (50 Hz):
┌──────────────────────────────────────────────────────┐
│ 1. Read CSV files (joint_pos, body_quat, etc.) │
│ │
│ 2. Every 20ms (50Hz): │
│ ┌──────────────────────────────────────────┐ │
│ │ Planner assembles reference: │ │
│ │ - next motion reference frames from CSV │ │
│ │ - current robot joint positions │ │
│ │ - current robot joint velocities │ │
│ │ - body angular velocity │ │
│ │ - last actions │ │
│ └──────────────────┬───────────────────────┘ │
│ ↓ │
│ ┌──────────────────────────────────────────┐ │
│ │ Encoder (TensorRT GPU): │ │
│ │ 290D motion joints + velocities │ │
│ │ → 64D latent token │ │
│ └──────────────────┬───────────────────────┘ │
│ ↓ │
│ ┌──────────────────────────────────────────┐ │
│ │ Decoder (TensorRT GPU): │ │
│ │ latent + robot state → 29 joint targets │ │
│ └──────────────────┬───────────────────────┘ │
│ ↓ │
│ Send motor commands via DDS │
└──────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────┐
│ MuJoCo (Terminal 1): │
│ Receive motor commands (kp, kd, q_target) │
│ torque = kp*(q_target - q) + kd*(dq_target - dq) │
│ Step physics → render │
└──────────────────────────────────────────────────────┘
Direct Replay vs Neural Network
This is the critical distinction: SONIC does not directly replay your CSV. It uses your motion as a hint to a trained neural network. The encoder compresses the motion reference into a 64-dimensional latent token. The decoder combines that latent with the robot’s actual physical state to produce joint targets that are physically feasible. These targets respect balance, joint limits, and contact dynamics.
The policy decides how closely to follow your motion. It was trained primarily on locomotion data, so:
- Leg tracking: accurate. The policy saw thousands of walking/running examples in training.
- Arm gestures: may be attenuated. The policy is conservative about upper-body motions it did not see during training.
This is a feature, not a bug. The neural network keeps the robot balanced even if your motion CSV would make it fall with direct replay.
Summary
| Stage | What It Does | Command |
|---|---|---|
| Create | Define joint angles at 50 Hz, output CSVs | python create_motion.py |
| Verify | Direct replay in MuJoCo — no neural net | python visualize_motion.py --motion_dir wave/ |
| Deploy | Through SONIC neural network | bash deploy.sh --motion-data wave/ sim |
Create defines the intent. Verify shows that the intent looks right. Deploy runs it through a physics-aware neural network that keeps the robot on its feet.
Links
- Sim2Sim repo: github.com/avparkhi/GEAR-SONIC-Sim2Sim
- SONIC paper: arxiv.org/abs/2511.07820
- Model weights: huggingface.co/nvidia/GEAR-SONIC
More recent articles
- pi-durable, from the ground up - 20th October 2026
- Hermes Agent: The Self-Improving Story - 12th June 2026
- Claude Code Dynamic Workflows: Inside Out - 29th May 2026