Hardware Academy
Robotics

Simulate before you build

Write a controller in the browser simulator, then learn what the real world adds.

Day 10 · 60 min

After this lesson

  • You can write and test a controller in the robot simulator lab.
  • You can explain the sim-to-real gap and how to design around it.
  • You can name the professional simulators and what each is for.

Why simulate, and what simulation gets wrong

Why simulate first

In the browser lab a run takes seconds and resets instantly. On the real robot each test costs a battery, a reset, a rescue from under the sofa and now and then a broken servo horn. A simulator also makes runs repeatable: the same start pose and the same code give the same result, so two controllers can be compared fairly and every bug can become a regression test. Industry leans on this hard. Waymo reported more than 20 billion simulated miles by 2021, roughly a thousand for every mile driven on public roads, and Rudin and colleagues trained an ANYmal quadruped to walk in under 20 minutes on one GPU by simulating 4,096 robots in parallel in NVIDIA Isaac Gym.

The sim-to-real gap

A simulator is a model, and every model leaves something out. The usual suspects:

  • Friction and slip. Simulated wheels never slip. Real tires grip tile and carpet very differently and cannot deliver accelerations the motors can.
  • Motor dynamics. In simulation speed jumps to the command. A real N20 takes 50 to 100 ms to respond and does not turn at all below roughly 15% duty.
  • Noise and dropouts. An HC-SR04 jitters by about a centimeter, misses echoes from soft or angled surfaces and measures only every 60 ms. Reflectance sensors drift with sunlight, which is full of infrared.
  • Sensor geometry. A simulated range sensor is a thin ray. A real ultrasonic beam is a cone roughly 30° wide that reports the nearest thing anywhere inside it.
  • Latency. Sensing, filtering, computing and the motors' response add up to 100 to 200 ms. At 40 cm/s, 150 ms is 6 cm driven blind.
  • Power. A sagging battery slows the motors and, as lesson 26 showed, can reset the brain.

What the lab models

The robot simulator lab models the course robot as a differential drive with a 12 cm wheel base, a 7 cm radius and a top wheel speed of 40 cm/s, integrated exactly along arcs. It has three ultrasonic rays at +45°, 0° and −45°, measured from the robot's edge to the first wall or obstacle (the canvas draws rays beyond the 150 cm rated range in red as open, but the readings go all the way to the far wall); two line sensors 6 cm ahead and 5 cm apart; wheel encoders at 10 ticks per cm; and a yaw rate. Touching a wall or an obstacle stops the robot and sets crashed. It deliberately leaves out noise, cones, latency, slip and motor lag, so you learn the logic first; margins for the real world are your job.

Structure the controller: read, estimate, decide, act

Every tick does four things. Read: copy the sensor values. Estimate: filter them into a belief about the world, such as a median of the last three ranges, the time since the line was last seen, or a pose integrated from the encoders. Decide: a state machine picks a behavior. Act: the behavior's desired speed and turn rate become wheel commands, clamped to −1…1. Keep each step a small function and all state in the lab's memory object. The same shape ports straight to the firmware in lesson 26, where control(sensors, memory) becomes controlStep().

Testing and logging

Turn behaviors into scenarios with pass criteria: no crash in 60 s, a lap in under 45 s, never closer than 5 cm to a wall. Then randomize: start pose ±5 cm and ±10°, sensor noise, latency. A controller that passes 100 of 100 randomized runs is far more likely to survive the floor than one tuned on a single perfect run. Log every tick (time, sensors, state, commands) as CSV with the same columns the robot's telemetry uses, so you can overlay a simulated run and a real one and see exactly where they diverge.

Professional simulators

  • Gazebo (Open Robotics): the standard ROS 2 companion. It loads URDF and SDF models and simulates lidar, cameras and IMUs; Gazebo Classic reached end of life in January 2025 in favor of the new Gazebo releases such as Harmonic.
  • Webots (Cyberbotics): open source since 2018, with a large library of ready robot models; popular in teaching.
  • MuJoCo (DeepMind, open-sourced in 2022): fast, accurate contact dynamics; the standard for reinforcement learning on legs and hands.
  • NVIDIA Isaac Sim: PhysX physics and RTX ray-traced cameras and lidar for photorealistic synthetic data, with Isaac Lab for massively parallel RL.
  • PyBullet: Python bindings for the Bullet engine, quick to script for research prototypes.

URDF basics

ROS tools and most simulators describe a robot with URDF, an XML tree of link elements (visual, collision and inertial properties) joined by joint elements (fixed, continuous, revolute or prismatic), each with a parent, a child, an origin in meters and radians and an axis. The course robot is a base_link, two continuous wheel joints 6 cm either side of center and fixed joints for the caster and sensors. Gazebo extends it with SDF, MuJoCo uses its own MJCF format, and xacro macros keep the left and right wheels from being written twice.

course_robot.urdf (excerpt)xml
<robot name="course_robot">
  <link name="base_link">
    <visual><geometry><cylinder radius="0.07" length="0.04"/></geometry></visual>
    <collision><geometry><cylinder radius="0.07" length="0.04"/></geometry></collision>
    <inertial><mass value="0.45"/><inertia ixx="6e-4" iyy="6e-4" izz="1.1e-3" ixy="0" ixz="0" iyz="0"/></inertial>
  </link>

  <link name="left_wheel"><visual><geometry><cylinder radius="0.0215" length="0.012"/></geometry></visual></link>
  <joint name="left_wheel_joint" type="continuous">
    <parent link="base_link"/><child link="left_wheel"/>
    <origin xyz="0 0.06 0" rpy="-1.5708 0 0"/>   <!-- 6 cm to the left: wheel base 12 cm -->
    <axis xyz="0 0 1"/>
  </joint>
  <!-- right_wheel: the same with y = -0.06 -->

  <link name="sonar_link"/>
  <joint name="sonar_joint" type="fixed">
    <parent link="base_link"/><child link="sonar_link"/>
    <origin xyz="0.07 0 0.08" rpy="0 0 0"/>      <!-- front edge, 8 cm up -->
  </joint>
</robot>
Sensor model versus reality: a simulated ray against a real ultrasonic cone, with echo timing and timeoutSimulator: an ideal rayReal HC-SR04: a cone about 30° widepole: the ray misses itreads 120 cm, every timeno noise, no delay, no beam widthreads 59 cm: the pole, not the wallangled wall: the ping bounces away±1 cm noise, one reading per 60 ms, missed echoesTRIG10 µs pulse, then 8 cycles at 40 kHz leave the transducerECHOpole at 59 cm: high for 59 × 58 µs = 3.4 msECHOno echo: the line stays high for about 38 ms, then gives uptimeout
Sensor model versus reality. The simulated ray sees exactly one point. The real ultrasonic cone reports the nearest object inside it, angled walls can send the ping away, and a missing echo is a 38 ms timeout, not a distance.

Interactive: tour the arenas, then open the lab

The simulator's three arenas

The lab's own arenas and sensor model: the readings below are exactly what your controller would get at the robot's pose. Here the robot follows a scripted path; in the lab, your code drives it.

Open room arena preview with the robot and its three ultrasonic rayslab startOpen room: 300 cm × 200 cm · grid 50 cm · robot 14 cm across
Arena

Three round obstacles in an empty room: practice obstacle avoidance and wall following.

sensors.ultrasonic: left 168.8 cm (open) · front 52.1 cm · right 61.9 cm

Rays beyond the 150 cm rated range are drawn red, as in the lab; their readings still run to the far wall.

A live copy of the simulator running the obstacle-avoid preset in the open room. Press Run, watch the rays, then open the full lab to edit the controller.

Robot simulator

Obstacle avoid: Cruise, slow down when something is ahead, and turn away from the nearest short ray.

Simulated robot in the Open room arena
Preset
Arena
Speed

Ready. Press Run.

Lab goal: 0 / 100 cm without a crash

Ready to drive it yourself?

The lab runs your JavaScript control(sensors, memory) function 100 times a second against the same three arenas, with telemetry and the sensor rays drawn live.

Open the robot simulator lab →

In practice

Practice

Obstacle avoidance, then line following, then a lap without a crash

  1. Open the robot simulator lab, choose the open room and write an obstacle avoider. Pass: 60 s without a crash.
  2. Switch to the line track and implement the line-following state machine from lesson 25. Pass: one full lap without leaving the line for more than 1 s and without a crash.
  3. Make it realistic: put your avoider behind the latency wrapper below (300 ms) and press Run five or six times; every run starts from a random heading. Then fix the controller without removing the latency.

Worked answer, part 1. Braitenberg-style steering on the difference between the side rays, plus an escape spin that commits to one direction with hysteresis, so the robot neither stalls between two obstacles nor dithers in corners:

obstacle-avoid.jsjs
// Part 1 - obstacle avoidance for the open-room arena.
// sensors.ultrasonic.{left, front, right}: cm from the robot's edge to the first wall or obstacle along each ray.
// Return wheel commands in -1..1 (1 = 40 cm/s).
function control(sensors, memory) {
  const { left, front, right } = sensors.ultrasonic;
  const p = (d) => Math.max(0, 1 - d / 60);          // proximity: 0 beyond 60 cm, 1 at contact

  // Blocked ahead: spin toward the more open side. Pick the side once and keep it until the front
  // reads more than 35 cm (hysteresis: in at 25, out at 35), so the robot cannot flip back and forth.
  if (front < 25 || (memory.spin && front < 35)) {
    if (!memory.spin) memory.spin = left > right ? 1 : -1;   // +1 = turn left
    return { left: -0.5 * memory.spin, right: 0.5 * memory.spin };
  }
  memory.spin = 0;

  // Cruise, slow down when something is ahead, and steer away from the closer side. Steering on
  // the difference keeps the robot moving when both sides are equally close, instead of stalling.
  const base = front < 60 ? 0.35 : 0.6;
  const turn = 0.6 * (p(right) - p(left));           // obstacle on the right: turn left
  return { left: base - turn, right: base + turn };
}

Part 2 is the state machine from lesson 25, unchanged: follow with P control on the line error, recover toward the side the line was last seen, stop after 3 s of searching. If it oscillates on the straights, lower the steering gain; if it loses the line on the corners, lower the base speed first.

latency-wrapper.jsjs
// Part 3 - run a controller on delayed sensors. Rename your control() to avoid() and paste it above this.
// The lab steps at 100 Hz, so the delay is LATENCY_MS / 10 ticks. Each run first spins for a random
// time, so every press of Run starts from a different heading: count crashes over several runs.
const LATENCY_MS = 300;
function control(sensors, memory) {
  if (memory.spinFor === undefined) memory.spinFor = Math.random() * 1.9;   // up to one full turn
  if (sensors.t < memory.spinFor) return { left: -0.5, right: 0.5 };
  memory.queue = memory.queue || [];
  memory.queue.push(JSON.parse(JSON.stringify(sensors)));
  const delayed = memory.queue.length > LATENCY_MS / 10 ? memory.queue.shift() : memory.queue[0];
  return avoid(delayed, memory);
}

Part 3, what you should see. At 0 and 150 ms nothing visible changes. At 300 ms roughly one run in four ends with Crashed: yes within a minute (270 of 1,000 randomized runs in our tests), and always the same way: the robot cruises toward the small obstacle near the top-right corner, and steering that reacts 0.3 s late cannot get it clear. The escape spins overshoot too: at a command of 0.5 the robot turns at 191°/s, so it keeps spinning about 57° past the gap it saw. Pause and step through a spin while watching θ in the telemetry to see it.

The fix follows the margin rule: every gain inside a delayed loop has to shrink, and every margin has to grow with speed × delay. Slow the cruise from 0.6 to 0.4, halve the spin rate and the steering gain, and widen the thresholds:

obstacle-avoid-with-latency.jsjs
// Part 3 answer - the Part 1 avoider retuned for 300 ms of latency (keep the wrapper above it).
function avoid(sensors, memory) {
  const { left, front, right } = sensors.ultrasonic;
  const p = (d) => Math.max(0, 1 - d / 70);          // react from 70 cm instead of 60

  if (front < 30 || (memory.spin && front < 45)) {   // wider margins: in at 30, out at 45
    if (!memory.spin) memory.spin = left > right ? 1 : -1;
    return { left: -0.25 * memory.spin, right: 0.25 * memory.spin };   // half the spin rate
  }
  memory.spin = 0;

  const base = front < 70 ? 0.25 : 0.4;              // two thirds of the speed
  const turn = 0.3 * (p(right) - p(left));           // half the steering gain
  return { left: base - turn, right: base + turn };
}

In our tests this version did not crash once in 1,000 randomized runs at each of 0, 150, 300 and 500 ms. One surprise is worth knowing: the Part 1 answer also survives 500 ms from this start, because whether a late reaction hits something depends on where the robot happens to be when it is late. That is exactly why teams randomize latency over a range instead of testing one value.

Real-world scenario

Real-world scenario

Perfect in simulation, crashes on the real floor

A team's wall-following controller passed 200 out of 200 runs in their simulator at 40 cm/s with a 15 cm front threshold. On the real robot, in the office hallway, it clips the wall at about one corner in three and has twice driven into the base of a whiteboard stand.

Symptoms

  • The real robot starts its corner turn 6 to 9 cm later than the simulated robot does
  • Logs show the front range updating only every 60 ms, with occasional 400 cm readings next to a wall angled at 45°
  • At 20 cm/s the real robot almost never touches a wall
  • The thin metal legs of the whiteboard stand are invisible to it until it is too late

Interview questions

screenWhat is the sim-to-real gap? Give three concrete sources.
It is the difference between how a controller behaves in simulation and on hardware, caused by everything the model leaves out or gets wrong. First, actuation: simulated wheels reach the commanded speed instantly and never slip, while real gearmotors have a time constant of tens of milliseconds, a deadband at low duty and slip on smooth floors. Second, sensing: a simulated ultrasonic sensor is a perfect ray, while a real one is a 30° cone with noise, dropouts and specular reflections off angled walls. Third, timing: real sensors update at their own rates and the whole loop has latency, which the simulator usually ignores. I close the gap by modeling the worst of these, randomizing them, and validating on hardware early.
screenName three robotics simulators and say what each is best for.
Gazebo is the default companion to ROS 2: it loads URDF and SDF models, simulates lidar, cameras and IMUs, and runs the same nodes you deploy, which makes it good for full-stack integration testing. MuJoCo, open-sourced by DeepMind in 2022, has fast and accurate contact dynamics and is the standard for reinforcement learning on legged robots and manipulators. NVIDIA Isaac Sim uses PhysX and RTX ray tracing for photorealistic sensors, so it is used for synthetic training data and large-scale parallel RL with Isaac Lab. For quick education or a single robot, Webots and PyBullet are lighter alternatives.
screenWhat is URDF, and what goes into one?
URDF, the Unified Robot Description Format, is an XML file that describes a robot as a tree of links connected by joints. Each link can have visual geometry, collision geometry and inertial properties; each joint has a type (fixed, continuous, revolute or prismatic), a parent and a child link, an origin with xyz and roll-pitch-yaw, and an axis. ROS tools such as robot_state_publisher use it to publish the TF tree, RViz uses it to draw the robot, and Gazebo uses it, often extended with SDF tags, to simulate it. People usually write it with xacro macros so the two wheels are one macro called twice.
deepYour robot must stop before obstacles at 0.5 m/s. How do you choose the detection threshold?
I would build a latency budget instead of guessing. Say the sensor updates every 60 ms, the filter adds 50 ms, the control period is 10 ms and the motors need 80 ms to respond: that is about 200 ms, during which the robot travels 0.5 × 0.2 = 10 cm. Braking from 0.5 m/s at 2 m/s² takes v²/2a = 6.25 cm. Adding 10 cm of clearance for noise and the robot's own shape gives a threshold of about 26 cm, and I would make it a function of speed so it shrinks when the robot is slow. Then I would verify it in a simulator with that latency modeled and on hardware with a soft obstacle.
deepHow would you use domain randomization so a controller trained or tuned in simulation works on hardware?
I identify the parameters that differ between simulation and reality and are uncertain: friction, motor gain and time constant, mass, sensor noise, latency, dropout rate, sensor mounting angle and start pose. For each I pick a range that brackets the real value, for example 50 to 200 ms of latency, and draw new values every episode, so the controller has to work across the whole range rather than at one point. The ranges should come from measurements on the real robot where possible, because ranges that are too wide make the task impossible and ranges that are too narrow miss the real world. I then measure success as a distribution over hundreds of randomized runs and only move to hardware when the worst decile is acceptable.

Quiz

1. A robot drives at 40 cm/s and its sensing-to-motor latency is 150 ms. How far does it travel before it starts reacting to a new obstacle?
2. A thin pole sits 10° off the robot's heading, closer than the wall straight ahead. What does a real HC-SR04 most likely report?
3. Which simulator, open-sourced by DeepMind in 2022, is known for fast contact dynamics in reinforcement learning research?
4. In URDF, which joint type models a wheel that spins forever with no limits?
5. Why randomize start poses, noise and latency across many simulated runs?
6. The sonar times out with no echo. What should the controller assume?

References and further reading

Where the ideas in this lesson come from, and what to read next. Books are the standard texts; papers are the original sources; standards are what employers work to.

  1. Paper

    Josh Tobin, Rachel Fong, Alex Ray, Jonas Schneider, Wojciech Zaremba and Pieter Abbeel (2017). Domain Randomization for Transferring Deep Neural Networks from Simulation to the Real World. IEEE/RSJ International Conference on Intelligent Robots and Systems (IROS).doi:10.1109/IROS.2017.8202133

    Read: Why randomizing the simulator makes controllers transfer.

  2. Bookfree online

    Nikolaus Correll, Bradley Hayes, Christoffer Heckman and Alessandro Roncone (2022). Introduction to Autonomous Robots: Mechanisms, Sensors, Actuators, and Algorithms. MIT Press.

    Read: The simulation exercises.

  3. Book

    Sebastian Thrun, Wolfram Burgard and Dieter Fox (2005). Probabilistic Robotics. MIT Press.

    Read: The motion and measurement model chapters, which are what a simulator implements.