Sim-to-Real for Humanoids: Deployment Best Practices
Complete pipeline for deploying RL locomotion policies to real humanoid robots — domain randomization, system ID, safety, and Unitree SDK.
Nguyễn Anh TuấnApril 9, 20269 min readUpdated: Jun 16, 2026
The Final Step: From Sim to Real Robot
Throughout the previous 9 posts, we trained humanoids to walk (G1, H1), run (H1 advanced), carry objects (loco-manip), and parkour (post 9) — all in simulation. Now comes the most important and difficult step: deploying to a real robot.
Sim-to-real for humanoids is harder than for quadrupeds (see locomotion sim2real) because: bipedal is inherently unstable, narrow support polygon, higher center of mass, and more DOF. A small mistake can cause the robot to fall and damage hardware worth tens of thousands of dollars.
Humanoid robot deployment
Domain Randomization Checklist for Humanoids
Physical Parameters
class HumanoidDomainRandomization:
"""Domain randomization config for humanoid sim-to-real."""
def __init__(self):
self.params = {
# --- Body dynamics ---
"base_mass_range": [0.85, 1.15], # ±15% total mass
"link_mass_range": [0.9, 1.1], # ±10% per link
"com_offset_range": [-0.02, 0.02], # ±2cm CoM offset
"inertia_range": [0.8, 1.2], # ±20% inertia
# --- Joint properties ---
"joint_friction_range": [0.0, 0.05],
"joint_damping_range": [0.5, 2.0],
"joint_armature_range": [0.01, 0.05],
"joint_stiffness_range": [0.0, 5.0],
# --- Actuator ---
"motor_strength_range": [0.85, 1.15], # ±15% max torque
"motor_offset_range": [-0.02, 0.02], # Position offset (rad)
# --- Ground ---
"ground_friction_range": [0.4, 1.5],
"ground_restitution_range": [0.0, 0.3],
# --- Delays & noise ---
"action_delay_range": [0, 30], # ms
"observation_delay_range": [0, 15], # ms
"action_noise_std": 0.02,
"joint_pos_noise_std": 0.01,
"joint_vel_noise_std": 0.1,
"imu_noise_std": 0.05,
"imu_bias_range": [-0.1, 0.1],
# --- External perturbations ---
"push_force_range": [0, 50], # N
"push_interval_range": [5, 15], # seconds
"push_duration": 0.1, # seconds
# --- Terrain ---
"terrain_roughness_range": [0.0, 0.03],
}
def randomize(self, env):
"""Apply randomization to environment."""
for key, (low, high) in self.params.items():
value = np.random.uniform(low, high)
env.set_param(key, value)
return env
Sim vs Real Gap Comparison
Parameter
Sim Default
Real Robot
Gap
Impact
Control latency
0ms
5-20ms
Critical
Oscillation, instability
Joint friction
0
0.01-0.05 Nm
Medium
Slow response
Ground friction
1.0
0.3-1.2
High
Slip, fall
Motor strength
100%
85-95%
Medium
Weaker motions
Sensor noise
0
IMU: ±2°, encoder: ±0.5°
Medium
Jerky control
Link mass
CAD exact
+5-10% (cables, etc.)
Medium
Balance offset
Flexibility
Rigid
Harmonic drives flex
High
Compliance mismatch
System Identification
Motor Identification
import numpy as np
from scipy.optimize import minimize
class MotorIdentification:
"""Identify real motor parameters from data."""
def __init__(self, joint_name, dt=0.002):
self.joint_name = joint_name
self.dt = dt
def collect_data(self, robot, duration=10.0):
"""Collect torque-position-velocity data."""
data = {'pos': [], 'vel': [], 'torque': [], 'cmd': []}
t = 0
while t < duration:
freq = 0.5 + t / duration * 5.0 # Chirp 0.5 → 5.5 Hz
cmd = 0.3 * np.sin(2 * np.pi * freq * t)
robot.set_joint_position(self.joint_name, cmd)
state = robot.get_joint_state(self.joint_name)
data['pos'].append(state.position)
data['vel'].append(state.velocity)
data['torque'].append(state.effort)
data['cmd'].append(cmd)
t += self.dt
return {k: np.array(v) for k, v in data.items()}
def identify(self, data):
"""Fit motor model to collected data."""
pos, vel, torque, cmd = data['pos'], data['vel'], data['torque'], data['cmd']
def motor_model(params, cmd, pos, vel):
Kp, Kd, friction, damping = params
return Kp * (cmd - pos) - Kd * vel - friction * np.sign(vel) - damping * vel
def objective(params):
pred = motor_model(params, cmd, pos, vel)
return np.mean((pred - torque) ** 2)
x0 = [100.0, 5.0, 0.5, 0.1]
bounds = [(10, 500), (0.1, 50), (0, 5), (0, 2)]
result = minimize(objective, x0, bounds=bounds, method='L-BFGS-B')
Kp, Kd, friction, damping = result.x
print(f"Joint {self.joint_name}: Kp={Kp:.1f}, Kd={Kd:.2f}, "
f"friction={friction:.3f}, damping={damping:.3f}, MSE={result.fun:.6f}")
return {'Kp': Kp, 'Kd': Kd, 'friction': friction, 'damping': damping}