
Which simulators contain an SO-100, what domain randomization really costs in compute, and how large the sim-to-real gap measures in published paired evaluations.
An SO-100 costs about 110 to 150 EUR in parts, and every hour you spend driving it by hand is an hour spent on nothing else. A simulated arm costs nothing per attempt, never strips a gear, and runs a thousand copies of itself on one GPU. The question is what you get back when you carry a simulated policy over to a real arm.
The short version
- •Simulation buys cheap trials, not a free policy. The best published SO-101 result falls from 96.1 percent in simulation to 91.3 percent on hardware, and the two hardest tasks fall 15 and 21 points.
- •The arm does exist in simulation: an MJCF in MuJoCo Menagerie, a ready SO100GraspCube-v1 task in ManiSkill3, an SO-101 course from NVIDIA. None ships your arm, with your backlash.
- •Domain randomization is a tax paid in compute: OpenAI needed roughly 3 years of simulated experience without it and roughly 100 years with it.
- •Simulation is strongest for reinforcement learning and repeatable evaluation. The five policies here are imitation learners, and a physics engine does not produce demonstrations.
- •Validation loss is a poor stand-in for real performance: r = 0.308 in the SIMPLER study, against r = 0.924 for a matched simulated evaluation.
The honest answer, from published numbers rather than launch posts: a great deal for some task shapes, close to nothing for others. Below, which simulators contain an SO-100, what randomization costs, how large the gap measures, and where it will not help an imitation learning project.
How big is the reality gap, in numbers
Several groups have published paired evaluations: the same trained policy scored in simulation and then on the physical robot under the same task definition. Those pairs are the only honest measurement of the gap.
| System | Task | Simulation | Hardware |
|---|---|---|---|
| Squint on SO-101, Feb 2026 | 8 tasks, average | 96.1 percent | 91.3 percent (73 of 80) |
| Squint on SO-101 | Reach Cube | 100 percent | 10 of 10 |
| Squint on SO-101 | Stack Cube | 95.0 percent | 8 of 10 |
| Squint on SO-101 | Stack Can | 81.2 percent | 6 of 10 |
| PPO baseline, same setup | 8 tasks, average | 60.2 percent | 62.5 percent (50 of 80) |
| OpenAI Dactyl, Shadow Hand | Block rotations, vision | median 33 | median 11.5 |
| OpenAI Dactyl, Shadow Hand | Block rotations, state | median 50 | median 13 |
Reaching transfers essentially perfectly, stacking does not. The gap grows with contact precision, and fastest where a low-cost arm is weakest: holding a pose while the gripper closes. The Dactyl rows give the scale of the problem on far better hardware.
The average gap above is under 5 points, which sounds solved. Split by task and the reach tasks contribute nothing while Stack Can contributes 21. If your task ends with a tolerance smaller than the arm's own repeatability, budget for the tail. From the real side this is the policy only works in one setup failure.
The four gaps, and which one bites a hobby arm
NVIDIA's SO-101 course splits the gap into sensing, actuation, physics and modeling. On an SO-100 the four are not equally dangerous. Three you can attack with randomization. The fourth is hardware.
| Gap | What it means | How it shows up on an SO-100 |
|---|---|---|
| Sensing | Camera models, lighting and exposure are not physically faithful | Your webcam has rolling shutter and auto exposure. The renderer has neither, so the image distribution shifts when a cloud passes |
| Actuation | Motor models omit friction, backlash, thermal drift and delay | STS3215 servos run their own position loop at 7.4 V. The simulator drives an idealised actuator, so commanded and executed paths diverge |
| Physics | Contact, friction and deformables are approximations | A printed jaw on a smooth cube is friction guesswork. Cable, cloth and anything soft is outside what these engines model |
| Modeling | The URDF is not the machine on your desk | Printer tolerances, assembly slop, hand-tightened screws. The published description is one arm; yours has its own play |
NVIDIA is blunt about the actuation term: these are hobby servos, they introduce backlash, and it accumulates along the kinematic chain. The course's answer is SAGE, a Tongji University, Peking University and NVIDIA project that replays the same commanded motion in both domains and compares position, velocity and torque per joint. Bridging what it measures needs a separate learned actuator model, GapONet, and the course lists compensation inside the deployment loop as future work. Measurement comes first because the motor model is what randomization cannot fix.
Check the arm before you blame physics. The lerobot-sim2real tutorial spends its first step here: set the zero position accurately and move every joint to its true mechanical extreme. A sloppy zero offsets every simulated joint angle by a constant and fails identically to a transfer failure. See calibration and arm twitches then sags. Keep the rail at 7.4 V: 12 V destroys STS3215 servos.
Which simulator actually contains an SO-100
This is where people lose a weekend, because the answer differs between the SO-100 and the SO-101, and between an arm description and a usable task. Checked on 23 August 2026.
| Where | What you get | Version checked | The catch |
|---|---|---|---|
| MuJoCo Menagerie, trs_so_arm100 | Simplified MJCF of the 5-DoF Standard Open Arm-100 | Model v1.3, MuJoCo 3.1.6+, Apache-2.0 | Simplified: hand-made gripper collision meshes, extra box geoms, position actuators added on conversion |
| SO-ARM100 repo, Simulation/ | SO100: one URDF. SO101: URDF plus MJCF, two calibration variants | main branch, 23 Aug 2026 | Base collision meshes removed for misbehaving in sim. The LeRobot gripper convention is in neither file |
| ManiSkill3 | SO100GraspCube-v1, a digital-twin task with randomization and greenscreening built in | ManiSkill3, RSS 2025 | Needs an NVIDIA GPU with working Vulkan, the fast part and the fragile part |
| Isaac Lab plus LeIsaac | SO-101 teleoperation in Isaac Sim, HDF5 recording, conversion to LeRobot format | v3.0.0-beta2.patch1, 2 July 2026 | A beta line. The Newton backend is in active development and not every environment has a Newton preset yet |
| gym-hil, LeRobot's own sim | MuJoCo pick-cube environments, PandaPickCubeGamepad-v0 | current main | A Franka Panda, not an SO-100. Fine for the tooling, useless as a twin |
In LeRobot the gripper is a linear joint where 0 is closed and 100 is open. The SO-101 simulation README states that this mapping is not yet reflected in the URDF and MuJoCo files. Replay a trajectory into the simulator and the gripper can do the opposite of what you expect: gripper does not close.

Domain randomization, with the real default values
Domain randomization comes from Tobin et al. in 2017: rather than make one simulator accurate, make many varied enough that reality looks like another sample. That paper trained a real-world object detector accurate to 1.5 cm from simulated images with non-realistic random textures. Peng et al. extended it to dynamics the same year.
The abstract version is not useful. Here is what the ManiSkill3 SO-100 grasping task randomizes, from the environment source.
# mani_skill/envs/tasks/digital_twins/so100_arm/grasp_cube.py
@dataclass
class SO100GraspCubeDomainRandomizationConfig:
# task agnostic
initial_qpos_noise_scale: float = 0.02
robot_color: Optional[Union[str, Sequence[float]]] = None # set "random" to vary
randomize_lighting: bool = True
max_camera_offset: Sequence[float] = (0.025, 0.025, 0.025) # metres, per axis
camera_target_noise: float = 1e-3
camera_view_rot_noise: float = 5e-3
camera_fov_noise: float = np.deg2rad(2) # 2 degrees
# task specific, applied while the scene loads
cube_half_size_range: Sequence[float] = (0.022 / 2, 0.028 / 2)
cube_friction_mean: float = 0.3
cube_friction_std: float = 0.05
cube_friction_bounds: Sequence[float] = (0.1, 0.5)
randomize_cube_color: bool = True
@register_env("SO100GraspCube-v1", max_episode_steps=64)
class SO100GraspCubeEnv(BaseDigitalTwinEnv):
def _default_sim_config(self):
return SimConfig(sim_freq=100, control_freq=20)Three numbers there are worth stealing. The camera may move 2.5 cm on any axis and its field of view may wander 2 degrees, which is the tolerance you are granted for aligning a real camera by hand. The cube is not one cube: half size from 1.1 to 1.4 cm, friction centred on 0.3 and clipped to 0.1 to 0.5. Physics runs at 100 Hz, control at 20 Hz.
NVIDIA's course randomizes wider because it generates demonstrations rather than running RL: exposure -4.0 to 3.0, colour temperature 2500 to 9500 K, HDRI backgrounds, camera offsets of 2 cm in x and y and 1 cm in z, camera rotation of 0.05 radians per axis, plus object mass, friction and restitution, joint damping and limits, and actuator delays.
OpenAI measured this on in-hand cube rotation: about 3 years of simulated experience with no randomization, about 100 years fully randomized, roughly 1.5 against 50 hours of wall clock. Turning every knob on is not a safe default. Randomize the axis you cannot control on the bench, measure, then add the next. The ManiSkill randomization tutorial lists which properties are fixed once the scene loads.
The manual path: reinforcement learning in ManiSkill, deployed zero-shot
The shortest route ending with an SO-100 picking up a cube from a policy trained without a single real demonstration. It comes from the lerobot-sim2real tutorial by Xander Hinrichsen and Stone Tao, built on ManiSkill3. You need an NVIDIA GPU with at least 8 GB, working Vulkan, one camera and an assembled arm.
- 1Install the simulator, then pin LeRobot
ManiSkill and SAPIEN depend on NVIDIA drivers and Vulkan, and the demo command tells you whether yours work. The tutorial pins LeRobot to commit a989c795587d122299275c65a38ffdd0a804b8dc because import paths moved upstream.
bashconda create -n ms3-lerobot "python==3.11" git clone https://github.com/StoneT2000/lerobot-sim2real.git cd lerobot-sim2real pip install -e . pip install torch python -m mani_skill.examples.demo_random_action - 2Place the camera in simulation first, not on the desk
The robot spawns at the origin with the table top at z = 0, so camera position in env_config.json is metres from the arm base. Record the reset distribution to see where cubes actually spawn.
bashpython lerobot_sim2real/scripts/record_reset_distribution.py \ --env-id="SO100GraspCube-v1" \ --env-kwargs-json-path=env_config.json - 3Nudge the real camera until the overlay lines up
This overlays the simulated render on the live feed and tunes field of view with the arrow keys. The tutorial calls it the hardest step and reports that worse alignment transfers worse. Randomization covers a margin of error, not a mistake.
bashpython lerobot_sim2real/scripts/camera_alignment.py \ --env-id="SO100GraspCube-v1" \ --env-kwargs-json-path=env_config.json - 4Photograph the empty table for greenscreening
Unmount the arm, shoot the background, point greenscreen_overlay_path at the result. The simulated foreground is composited onto your real background, removing most of the visual gap.
bashpython lerobot_sim2real/scripts/capture_background_image.py \ --env-id="SO100GraspCube-v1" \ --env-kwargs-json-path=env_config.json \ --out=greenscreen.png - 5Train PPO on rendered images
The tuned script uses 8 to 10 GB of GPU memory at 1024 parallel environments. The flag says 100 million timesteps, but the authors report 25 to 40 million is enough, about an hour on a 4090, and that training longer sometimes produces worse policies. For your own task, see the custom task tutorial.
bashseed=3 python lerobot_sim2real/scripts/train_ppo_rgb.py \ --env-id="SO100GraspCube-v1" --env-kwargs-json-path=env_config.json \ --ppo.seed=${seed} \ --ppo.num_envs=1024 --ppo.num-steps=16 \ --ppo.update_epochs=8 --ppo.num_minibatches=32 \ --ppo.total_timesteps=100_000_000 --ppo.gamma=0.9 \ --ppo.num_eval_envs=16 --ppo.num-eval-steps=64 \ --ppo.no-partial-reset \ --ppo.exp-name="ppo-SO100GraspCube-v1-rgb-${seed}" - 6Run the checkpoint on the arm
Use a cube around 2.5 cm, the middle of the trained size range, inside the spawn region from step 2. The tutorial recommends control frequency 15 on SO-100 hardware.
bashpython lerobot_sim2real/scripts/eval_ppo_rgb.py \ --env_id="SO100GraspCube-v1" \ --env-kwargs-json-path=env_config.json \ --checkpoint=path/to/ckpt.pt \ --no-continuous-eval --control-freq=15
Keep --no-continuous-eval on for the first deployment; it makes the arm wait for a keypress before each action. Reinforcement learning finds behaviours that score well and look nothing like a demonstration, which on a printed gripper can mean driving the end effector into the table. Keep a hand on ctrl+c, which stops the script and disables torque. Test more than one checkpoint: several with identical simulated scores behave differently on hardware.
Two routes to a policy that works on your desk
Build a digital twin of your workspace, train with reinforcement learning against a reward function, deploy zero-shot. It is the only practical way to get millions of interactions on an arm that costs 110 to 150 EUR in parts.
- One NVIDIA GPU with 8 GB or more, correct drivers, working Vulkan. This is the most common failure, not the RL.
- An afternoon of camera alignment, to be redone after you move the desk.
- About an hour of PPO per task on a 4090, plus the runs you throw away.
- A reward function. For SO100GraspCube-v1 it is roughly five lines, which is why it does not generalise.
- Acceptance that the result is one task, with no language conditioning and no transfer to the next object.
The lerobot-sim2real author recommends reproducing squint instead: same base, faster training, better transfer, eight SO-101 tasks rather than one. Published result 96.1 percent in simulation and 91.3 percent over 80 real trials, after 15 minutes on a single RTX 3090.
There is no simulator on this platform, and pretending otherwise would waste your time. AY-Robots is the real-robot half of the loop: drive an arm, record LeRobot datasets, train one of five policies on a rented GPU, four of them vision-language-action models, serve the result back to the arm.
- Drive a physical SO-100 from the browser at /live, no signup, queue-based, before you model it.
- Record episodes with the desktop client from /download, straight out of a teleoperation session.
- Bring in a dataset from a Hugging Face repo id or your own machine. A simulator-generated dataset in LeRobot format is just a dataset.
- Pick model and hyperparameters on /train. The backend rents a GPU by required VRAM and writes checkpoints to object storage.
- Serve the policy from an auto-provisioned pod with an idle watchdog, so nothing bills quietly.
The 24 GB tier for SmolVLA and ACT costs about 1 to 3 USD per run; the A100 or H100 tier for GR00T N1.7 and Pi0.5 about 4 to 12 USD. See pricing.
What simulation will not do for this project
The gap between what simulation is good at and what an SO-100 project needs is wider than the sim-to-real gap itself. Reinforcement learning wants a reward function and millions of resets. Fine-tuning a VLA wants demonstrations of a task you can name in a sentence. Only the first is what a physics engine is built for.
- Trials and failures are free. The route above burns 25 to 40 million environment steps for one cube grasp, which no physical arm survives.
- You can vary what reality will not: cube size, friction, lighting, background, camera field of view, robot colour.
- Reset is instant. In real data collection the human resetting the scene is the bottleneck.
- Evaluation repeats exactly. On hardware you cannot re-run yesterday's trial.
- It is the only realistic way to run reinforcement learning on this class of hardware.
- The simulated arm is not your arm. Menagerie's model is simplified, the SO-101 description dropped its base collision meshes, and the gripper convention is in neither file.
- Randomization multiplies compute, roughly 33x in the one case where both numbers were published.
- It needs an NVIDIA GPU with working Vulkan, often a second machine or a rented one.
- Camera alignment is manual, and with joint calibration it is the first thing to suspect when transfer fails.
- None of the five trainable policies learn from a reward function. A simulator changes what you teleoperate, it does not remove it.

Feeding simulated data to a real policy
The bridge between the two worlds is the dataset, not the policy. If your simulator writes episodes in LeRobot format, a trainer cannot tell them from recorded ones. LeIsaac does this for Isaac Lab: teleoperate in Isaac Sim, record HDF5, convert, fine-tune. NVIDIA's course then co-trains on a mixture.
The recommended mixture is deliberately lopsided: about 5 real episodes against 70 to 100 simulated ones. The real handful is not there for volume, it anchors the sensing and actuation terms the renderer got wrong. Their sim evaluation of a vial pick-and-place policy is quoted at 50 to 70 percent.
| Policy | Dataset format required | Minimum episodes | GPU tier |
|---|---|---|---|
| GR00T N1.7 | LeRobot v2.0 or v2.1 | 50 | A100 80 GB or H100 80 GB |
| GR00T N1.5 | LeRobot v2.0 or v2.1 | 50 | A100 80 GB or H100 80 GB |
| Pi0.5 | LeRobot v3.0 | 50 | A100 80 GB or H100 80 GB |
| SmolVLA | LeRobot v3.0 | 30 | RTX 4090 or any 24 GB card |
| ACT | LeRobot v3.0 | 50 | RTX 4090 or any 24 GB card |
A LeRobot v3.0 dataset crashes the GR00T loader and has to be converted down to v2.1. Simulation tooling emits whatever version its LeRobot pin produced, so a dataset that trains ACT can reject on GR00T N1.7 for reasons unrelated to your task. Start at dataset rejected as v3, not at your physics. Details in the dataset docs.
No arm on the desk yet?
Drive a real SO-100 in the browser before you spend a weekend building a digital twin of one. No signup, queue-based, and the same class of arm the public datasets were recorded on.
Drive a real armDoes a simulated score predict a real one?
This is the underrated use of simulation, and it pays off even if you never train in it. The SIMPLER study built simulated replicas of two real robot setups and ran paired evaluations of six checkpoints in both. The question was not whether success rates match in absolute terms, but whether they rank policies in the same order, which is all model selection needs.
| How you rank checkpoints | MMRV, lower is better | Pearson r, higher is better |
|---|---|---|
| Validation MSE on held-out episodes | 0.375 | 0.308 |
| SIMPLER, variant aggregation | 0.143 | 0.778 |
| SIMPLER, visual matching | 0.056 | 0.924 |
Averaged over three task families on the Google Robot setup. Sit with the top row: picking a checkpoint by validation loss, the habit carried over from supervised learning, correlates with real performance at r = 0.308. A matched simulated evaluation reaches 0.924. That is the quantitative version of the loss curve looking fine while the arm does nothing, which has its own failure page.
Two caveats the authors state themselves: the environments cover rigid objects only, and the greenscreening assumes a fixed camera and does not reproduce object shadows. Both apply to a desk with one webcam and a printed gripper.

That matters when you read a leaderboard. Most results on the arena come from simulated benchmarks. They signal relative capability, not what a checkpoint does on your desk under your lighting.
A decision rule that holds up
- Contact-light reaching or a coarse pick from a known spawn area, and you want reinforcement learning: simulate. This is what the tooling was built for.
- Precise contact, insertion, deformables, cable routing, or anything where the gripper must feel something: record real episodes.
- Comparing two checkpoints before spending arm time: a matched simulated evaluation ranks them better than validation loss.
- A VLA that follows a language instruction: record real demonstrations, then consider adding simulated ones. Start at train your first policy.
- One free evening, no NVIDIA GPU, an assembled arm: record 50 episodes. That is the minimum for four of the five policies and beats fighting Vulkan.
- Fast reactive task: none of this saves you from the control loop, 20 ms per action step for ACT and 485 ms for Pi0.5. See inference latency.
Simulation moves work, it does not delete it. On the RL route you trade demonstration hours for camera alignment, GPU time and reward shaping. If the task is geometric and repetitive, that trade is good. If it is the sort of thing you would show a person once, imitation learning from real episodes gets there faster, and how to collect high-quality VLA training data will do more than any randomization. On the simulator side, the Isaac Lab write-up covers the framework.
Can I train a policy entirely in simulation and skip recording on the real SO-100?▾
For a narrow geometric task with a reward function you can write down, yes. The ManiSkill3 route reaches a working cube grasp on a real SO-100 with no real demonstrations, and the SO-101 task set reports 91.3 percent over 80 real trials from pure simulation training. For a language-conditioned VLA task, no: those models learn from demonstrations, and a physics engine does not produce demonstrations without a scripted expert or a human teleoperating inside it.
Does AY-Robots run a simulator?▾
No. The platform covers teleoperation, dataset recording, cloud fine-tuning and serving the policy back to the arm. Run the physics engine yourself. The bridge is the format: anything that writes LeRobot v2.1 or v3.0 episodes can be imported from a Hugging Face repo id or your own machine and trained like any recorded dataset.
Which simulator should I start with for an SO-100?▾
ManiSkill3, because SO100GraspCube-v1 already exists and the path is documented end to end with working defaults. Use MuJoCo Menagerie's trs_so_arm100 for a lightweight MJCF when you want kinematics or a visual check without a GPU. Move to Isaac Lab plus LeIsaac for photorealistic rendering and teleoperated collection inside the simulator, accepting that the current line is a beta.
How much domain randomization is enough?▾
Start with the axis you cannot control on the bench: camera pose, lighting and object friction, which is what the ManiSkill3 defaults randomize at 2.5 cm of camera offset per axis, 2 degrees of field of view and cube friction around 0.3. Add axes only when a specific failure points at one, because every extra axis costs training time.
My policy hits 99 percent in simulation. What does that predict on hardware?▾
Less than you would like, and how much less depends on the task. In the SO-101 task set the reach tasks transferred at 100 percent while Stack Can fell from 81.2 percent in simulation to 6 of 10 on the arm. Treat a simulated success rate as necessary, not sufficient, and test several checkpoints with the same simulated score.
Sources
- Domain Randomization for Transferring Deep Neural Networks from Simulation to the Real World (Tobin et al., 2017)
- Sim-to-Real Transfer of Robotic Control with Dynamics Randomization (Peng et al., 2017)
- Learning Dexterous In-Hand Manipulation (OpenAI et al., 2018)
- Evaluating Real-World Robot Manipulation Policies in Simulation (SIMPLER, Li et al., 2024)
- ManiSkill3: GPU Parallelized Robotics Simulation and Rendering for Generalizable Embodied AI
- Isaac Lab: A GPU-Accelerated Simulation Framework for Multi-Modal Robot Learning
- Squint: Fast Visual Reinforcement Learning for Sim-to-Real Robotics (Almuzairee and Christensen, 2026)
- Step-by-step Guide for Zero-Shot RGB Sim2Real Manipulation with LeRobot
- MuJoCo Menagerie: Standard Open Arm-100 5DOF MJCF description
- SO-ARM100 repository: SO101 URDF and MJCF simulation files
- NVIDIA: What Is Sim-to-Real? (Train an SO-101 Robot From Sim-to-Real)
- NVIDIA: Domain Randomization and teleoperated data collection for the SO-101
- NVIDIA: Sim-to-Real Strategy 2, Co-Training With Real Data
- NVIDIA: Measuring and Closing the Gap With SAGE + GapONet
- ManiSkill documentation: Domain Randomization
Ready for high-quality robotics data?
AY-Robots connects your robots to skilled operators worldwide.
Get Started