Simulation and Digital Twins
Testing robots before they exist: simulators such as Gazebo, Webots, Isaac Sim and MuJoCo, robot descriptions in URDF, physics engines, the gap between simulation and reality, and digital twins of robot cells.
Why simulate
Robot simulation is running a robot's software against a virtual robot and a virtual world instead of real hardware: the simulator computes how the robot's joints and wheels move, what its sensors would measure, and what happens when it touches something. The software under test usually cannot tell the difference — it receives sensor messages and sends commands exactly as on the real robot.
Reasons to simulate first:
- The robot does not exist yet. Cells are designed, and software is written, before the hardware is delivered.
- Safety and cost. A bug that drives a simulated arm into a simulated table costs nothing. Collisions, falls and edge cases can be tested on purpose.
- Speed and scale. Simulations can run faster than real time and hundreds at once, which is how reinforcement learning gets millions of attempts (see AI in Robotics).
- Repeatability. The same scenario can run after every code change, as an automated test in a pipeline.
This topic has three small simulations of its own, each a single model rather than a full simulator: a two-link arm on Kinematics, a line-following robot on Motion Control and PID and a grid planner on Path Planning. The full simulators below add physics, sensors and many robots in 3D.
Physics engines
A physics engine is the part of a simulator that moves bodies according to the laws of mechanics: it applies forces and torques, integrates them into velocities and positions one small time step at a time, detects collisions between shapes and resolves contacts and friction. A robot simulator wraps a physics engine with robot models, sensor models (cameras, lidar, inertial measurement units) and an interface to the robot software.
Time in a physics engine moves in fixed steps, often one millisecond. In each step the engine turns acceleration into velocity and velocity into position — a numerical version of an integral. The step size matters. Drop a part from one meter and compare the simulated fall with the exact answer, t = \sqrt{2h/g}:
import math
g = 9.81 # m/s^2
height = 1.0 # drop a part from 1 m
exact = math.sqrt(2 * height / g)
print(f"exact: hits the floor after {exact:.4f} s")
for dt in (0.1, 0.01, 0.001):
t, y, v = 0.0, height, 0.0
while y > 0:
v -= g * dt # update the velocity first ...
y += v * dt # ... then the position with the new velocity
t += dt
print(f"dt = {dt:<6} hits the floor after {t:.4f} s")
exact: hits the floor after 0.4515 s dt = 0.1 hits the floor after 0.5000 s dt = 0.01 hits the floor after 0.4500 s dt = 0.001 hits the floor after 0.4520 s
A step of 0.1 s gets the time wrong by about 11%, and worse: a fast part can pass right through a thin wall between two steps, because it was in front of the wall at one step and behind it at the next. Engines therefore use small steps, and extra checks for fast objects.
The integration method matters too
The order of the two updates is not a detail. Explicit Euler computes the new position from the old velocity; semi-implicit (or symplectic) Euler first updates the velocity and then uses the new one:
For a mass on a spring — a stand-in for any springy joint or contact — the difference is dramatic. A real spring without friction keeps its energy; the explicit version invents energy at every step:
k, m = 100.0, 1.0 # spring stiffness (N/m) and mass (kg)
dt, steps = 0.01, 200 # 2 seconds of simulated time
def energy(x, v):
return 0.5 * k * x * x + 0.5 * m * v * v
def explicit_euler(x, v):
a = -k * x / m
return x + v * dt, v + a * dt # both from the OLD state
def semi_implicit_euler(x, v):
v = v + (-k * x / m) * dt # new velocity first
return x + v * dt, v # position from the NEW velocity
for name, step in (("explicit", explicit_euler), ("semi-implicit", semi_implicit_euler)):
x, v = 0.1, 0.0 # pulled 10 cm and let go
start = energy(x, v)
for _ in range(steps):
x, v = step(x, v)
print(f"{name:13} energy after 2 s: {energy(x, v) / start:.3f} x the start")
explicit energy after 2 s: 7.316 x the start semi-implicit energy after 2 s: 0.967 x the start
After two simulated seconds the explicit spring swings with more than seven times its starting energy; the semi-implicit one stays close. This is the "simulated robot that shakes itself apart" that every simulator user meets once. Some engines use exactly this semi-implicit step, others more sophisticated methods, but the lesson carries over: when a simulation vibrates or explodes, suspect the time step, stiff contacts and wrong masses before suspecting the robot code. Games face the same problem with frame time; see delta time in the Vibe Coding topic.
Simulators
Gazebo is the open-source 3D robot simulator developed alongside ROS (now governed, like ROS, by the Open Source Robotics Alliance): it simulates robots, sensors and environments with physics, and connects to ROS 2 through bridge packages. It is the default choice for ROS 2 projects. Like ROS, it comes in named releases, and each ROS 2 distribution has a recommended Gazebo release:
| Gazebo release | Released | End of life | Paired with ROS 2 |
|---|---|---|---|
| Fortress (LTS) | September 2021 | May 2027 | Humble |
| Harmonic (LTS) | September 2023 | May 2029 | Jazzy |
| Ionic | September 2024 | December 2026 | Kilted |
| Jetty (LTS) | September 2025 | May 2031 | Lyrical, Rolling |
As of October 2026 there is no 2026 Gazebo release; the next one is
planned for 2027. The command-line tool is gz:
gz sim shapes.sdf
Other widely used simulators, as of October 2026:
- Webots (Cyberbotics) — open source under the Apache 2.0 license since December 2018. The latest stable release is R2025a (February 2025).
- NVIDIA Isaac Sim — "built on NVIDIA Omniverse", with photorealistic rendering for training vision systems and Isaac Lab on top for robot learning. Its source is on GitHub under Apache 2.0 (some components under other terms); the latest version is 6.2.0 (14 October 2026). It is built around NVIDIA graphics hardware.
- MuJoCo — a fast physics engine and simulator aimed at research in control and learning. DeepMind (now Google DeepMind) acquired it in 2021 and made it open source under Apache 2.0 in 2022; the current version is 3.16.0 (14 October 2026), with variants that run many simulations in parallel on graphics processors (MJX and MuJoCo Warp).
- CoppeliaSim — a versatile simulator with Edu, Pro and Lite editions (version 4.10.0).
- Vendor tools — industrial robot makers ship their own simulation and offline programming software; see Industrial Robots and Cobots in Practice.
Describing a robot: URDF
A simulator, a motion planner and a visualization tool all need the same facts about a robot: its parts, how they are connected and how they can move. In the ROS world those facts live in a URDF file (Unified Robot Description Format): an XML description of a robot as a tree of links (rigid parts with shape, appearance, mass and inertia) connected by joints (with type, axis, position and limits). The same file feeds the simulator, MoveIt and the tf2 frame tree (see ROS 2). Gazebo's own world and model format is SDF (Simulation Description Format), which can also describe whole environments.
A shortened URDF for a two-link arm: a base, an upper arm of 0.30 m and a forearm of 0.20 m, both turning about the vertical z axis (the links and joints of a real arm, in the shape used on Kinematics, with the realistic 0.30 m and 0.20 m links of Anatomy of a Robot rather than the 1.0 and 0.7 of the simulation there):
<robot name="two_link_arm">
<link name="base_link"/>
<link name="upper_arm">
<visual>
<origin xyz="0.15 0 0"/>
<geometry><box size="0.30 0.04 0.04"/></geometry>
</visual>
<collision>
<origin xyz="0.15 0 0"/>
<geometry><box size="0.30 0.04 0.04"/></geometry>
</collision>
<inertial>
<origin xyz="0.15 0 0"/>
<mass value="0.5"/>
<inertia ixx="0.000133" iyy="0.003817" izz="0.003817"
ixy="0" ixz="0" iyz="0"/>
</inertial>
</link>
<joint name="shoulder" type="revolute">
<parent link="base_link"/>
<child link="upper_arm"/>
<origin xyz="0 0 0.10"/>
<axis xyz="0 0 1"/>
<limit lower="-2.6" upper="2.6" effort="10" velocity="1.5"/>
</joint>
<!-- forearm link and elbow joint: the same pattern, 0.20 m long,
with the elbow at xyz="0.30 0 0" on the upper arm -->
</robot>
Joint limits are in radians and radians per second, as everywhere in
ROS (2.6 rad is about 149°). The <inertial> block
is the one people get wrong most often: it is invisible in the 3D
view but decides how the physics engine moves the link. For a solid
box of mass m and sides
a, b, c (along
x, y, z), the moments of inertia about its
center are
mass = 0.5 # kg
length, width, height = 0.30, 0.04, 0.04 # m: a link along its x axis
ixx = mass / 12 * (width ** 2 + height ** 2)
iyy = mass / 12 * (length ** 2 + height ** 2)
izz = mass / 12 * (length ** 2 + width ** 2)
print(f'<inertia ixx="{ixx:.6f}" iyy="{iyy:.6f}" izz="{izz:.6f}"')
print(' ixy="0" ixz="0" iyz="0"/>')
<inertia ixx="0.000133" iyy="0.003817" izz="0.003817"
ixy="0" ixz="0" iyz="0"/>
The sim-to-real gap
The sim-to-real gap (also called the reality gap) is the difference between how a robot and its software behave in simulation and how they behave on the real hardware. Behavior tuned or learned in simulation can fail in reality because the simulator got something slightly wrong. Usual suspects:
- Friction and contact: the hardest physics to model; grippers and wheels depend on it.
- Motors and gearboxes: backlash, current limits, heating; a simulated motor delivers its torque instantly.
- Sensors: real cameras have noise, blur and changing light; real lidars see through glass and reflect off mirrors.
- Timing: network delay, a busy computer, a control loop that runs late.
- The world: cables, dust, a part that is 2 mm off, a person walking by.
Domain randomization
One answer is not to make the simulator perfect but to make it varied. Domain randomization trains or tests robot software in many simulated worlds whose properties — friction, masses, lighting, textures, sensor noise, delays — are drawn at random within plausible ranges, so that the real world looks like just one more variation. The technique is described in Tobin and colleagues' 2017 paper "Domain Randomization for Transferring Deep Neural Networks from Simulation to the Real World", which trained a neural network to locate objects using only simulated images with randomized textures and lighting, and which uses the term "reality gap".
The same idea works for plain testing. A mobile robot that brakes from 1.5 m/s stops in d = v^2 / (2\mu g), where \mu is the friction coefficient of the floor. A simulator tuned for one floor gives one answer; a randomized test shows the spread:
import random
import statistics
random.seed(42)
g = 9.81
speed = 1.5 # m/s: a mobile robot brakes from full speed
def braking_distance(mu):
return speed ** 2 / (2 * mu * g)
nominal = braking_distance(0.7) # the one floor the simulator was tuned for
print(f"nominal floor (mu = 0.7): {nominal:.3f} m")
# Domain randomization: every simulated run gets a different floor.
runs = [braking_distance(random.uniform(0.35, 0.9)) for _ in range(1000)]
print(f"randomized, mean: {statistics.mean(runs):.3f} m")
print(f"randomized, worst: {max(runs):.3f} m")
longer = sum(1 for d in runs if d > nominal)
print(f"runs longer than nominal: {longer} of {len(runs)}")
nominal floor (mu = 0.7): 0.164 m randomized, mean: 0.194 m randomized, worst: 0.327 m runs longer than nominal: 620 of 1000
On the tuned floor the robot stops in 16 cm; on a dusty, slippery one it needs twice that. A design that only ever saw the nominal floor would have underestimated the stopping distance in most runs. The randomness is seeded, as it should be in a test: a failing run can then be reproduced exactly.
Hardware in the loop
Between pure simulation and the real robot there are intermediate steps, shown in the diagram at the top of this page:
- Software in the loop: the real robot software, unchanged, runs against the simulator on an ordinary computer — the same nodes, topics and parameters as on the robot.
- Hardware in the loop: a real piece of hardware, typically the controller board or the programmable logic controller (PLC), is connected to a simulated machine. The controller cannot tell that the motors and sensors on the other side are a model, so its real timing, firmware and wiring are tested without moving anything.
Each step adds realism and cost. A good test plan runs many cheap simulations on every change, fewer hardware-in-the-loop tests per release, and the final acceptance on the real machine.
Digital twins of robot cells
A digital twin is more than a simulation: it is a virtual model that stays linked to one specific real system and is updated with its data. For a robot cell that means the model has the real cell's layout, the real robot programs and the real PLC logic, and receives measurements from the running cell. (A device twin on an IoT platform, by contrast, is mostly a status record; see device twin versus digital twin.)
What a twin of a cell is used for:
- Virtual commissioning: test the robot programs and the PLC logic against the model before the cell is built, so that start-up on site takes days instead of weeks. Vendors support this with virtual controllers that run the real robot software: KUKA's iiQKA.OS2, for example, includes what KUKA calls a complete virtual robot controller, and ABB describes its RobotStudio as "the world's most popular offline programming and simulation tool".
- Changes: try a new product variant, a faster path or a different layout on the twin first, then on the line.
- Monitoring: compare what the cell does with what the model predicts. A growing difference means something changed — a worn gripper, a slower conveyor, or a twin that is no longer up to date.
# Cycle times of one robot cell: what the digital twin predicts and what
# the real cell measured, in seconds, per shift.
twin = 41.8
measured = {"Mon": 42.1, "Tue": 42.3, "Wed": 42.0, "Thu": 43.9, "Fri": 44.6}
for day, seconds in measured.items():
deviation = (seconds - twin) / twin * 100
flag = " <- check the cell (or the twin)" if deviation > 3 else ""
print(f"{day}: {seconds:.1f} s {deviation:+.1f}%{flag}")
Mon: 42.1 s +0.7% Tue: 42.3 s +1.2% Wed: 42.0 s +0.5% Thu: 43.9 s +5.0% <- check the cell (or the twin) Fri: 44.6 s +6.7% <- check the cell (or the twin)
Two security points carry over from the OT topic. A detailed twin is a blueprint of the cell and its weaknesses and deserves the same protection as engineering drawings. And a twin that can write programs or settings back to the real controller is part of the control path: it falls under the same access control and change management as an engineering workstation (see IEC 62443).
Simulation in practice
- Start simple. A 2D simulation or a single function often answers the question; a photorealistic world can come later.
- Pin versions. Write down the simulator release, the ROS 2 distribution and the robot description version with every result; a simulator update can change physics behavior.
- Validate the model. Compare a few simulated motions with logs from the real robot (stopping distance, cycle time, joint torques) before trusting the simulation for decisions.
- Seed randomness and keep the failing cases. A scenario that failed once becomes a regression test.
- Watch the real-time factor. A simulator that cannot keep up runs slower than real time; robot software with timeouts may then fail for reasons that do not exist on the real robot.