AThe project
This first portfolio project builds a differential-drive simulator (a program that models a differential-drive robot moving in 2D and shows its trajectory), bringing together the Pass-1 geometry and motion lessons into something that runs. You implement the differential-drive kinematics (wheel speeds to motion), the motion model (step the pose forward over time), the coordinate frames (the robot's pose in the world), and visualisation (draw the path), and watch the robot drive straight, arc, and spin, observing how odometry tracks (and would drift). It's the natural first build: pure software, fully observable, and it makes the abstract math concrete.
The simulator is a small loop over the robot's state: its pose (x, y, ) in the world frame. Each timestep you take the commanded wheel speeds, apply the differential-drive kinematics to get the robot's linear and angular velocity (v, ), and apply the motion model to update the pose (move distance vdt along the heading, turn omegadt). Exactly the integration that real odometry does. Then you draw the robot and its trajectory. Driving the wheels in different patterns reproduces the kinematics you learned: equal speeds -> a straight line, unequal -> an arc, equal and opposite -> spinning in place. Building this cements how wheel commands become motion, how the motion model integrates pose, and how everything lives in a coordinate frame, and because it's a simulation, you can see and verify all of it (and it's the safe, cheap first step the testing lesson advocates).
Simulate a differential-drive robot: kinematics + motion model + frames + visualisation:
STATE: the robot's pose (x, y, theta) in the WORLD frame
EACH TIMESTEP dt:
1. take commanded WHEEL SPEEDS (vL, vR)
2. KINEMATICS -> v = (vR+vL)/2, omega = (vR-vL)/L (wheel speeds -> robot velocity)
3. MOTION MODEL -> integrate: x += v*dt*cos(theta); y += v*dt*sin(theta); theta += omega*dt (update pose)
4. VISUALISE -> draw the robot + append to the trajectory
TESTS: equal wheels -> straight | unequal -> arc | equal & opposite -> spin in place | observe odometry tracking
SKILLS: coordinate systems + motion models + kinematics + visualisation (the Pass-1 geometry/motion lessons, running)
The disciplines. Represent the robot's state as a pose (x, y, ) in the world frame. Each timestep: take the wheel speeds, convert to (v, ) with the differential-drive kinematics (, ), and integrate with the motion model (x += v dt cos theta; y += v dt sin theta; theta += omega dt) to update the pose: then visualise the robot and its trajectory. Verify against the kinematics you know (equal -> straight, unequal -> arc, opposite -> spin) and observe how odometry tracks the motion. The habits this builds: turn wheel commands into motion, integrate a motion model to move a pose, keep everything in a frame, and visualise to verify: the core loop of mobile-robot simulation. It makes Pass-1's geometry and motion concrete and runnable.
Formulas & method. Two formulas carry the whole simulator. The kinematics that turn wheel speeds into robot velocity, and the integration that turns velocity into a new pose:
v = (vR + vL) / 2.0 # linear velocity (m/s)
omega = (vR - vL) / L # angular velocity (rad/s), L = wheel separation
x += v * math.cos(theta) * dt # integrate in the WORLD frame, so the
y += v * math.sin(theta) * dt # heading has to multiply the displacement
theta += omega * dt
The method is verification by known cases rather than by eyeballing the plot: must give a straight line, a circular arc of radius , and a pure spin with and unchanged. Each of those is a hand-computable prediction, which is what makes a wrong sign convention visible instead of merely suspicious.
Everything else you already have, and this is where each piece comes from:
| Piece | Lesson | Call |
|---|---|---|
| the pose as state | Coordinate frames | x, y, theta in the world frame |
wheel speeds to (v, omega) |
Diff-drive kinematics | (vR + vL)/2, (vR - vL)/L |
| stepping the pose forward | Odometry & motion models | x += v*cos(theta)*dt |
| the fixed-step loop | Sense-think-act | for _ in range(steps): |
| the trajectory plot | Plotting (Python Turn 1) | plt.plot, plt.quiver, plt.savefig |
Project layout. Four files, because the kinematics must be testable without the plotting and the plotting must not be able to change the kinematics:
diffdrive-sim/
|-- pose.py # THE TYPE, and its UNITS. Plus the angle wrap.
|-- kinematics.py # wheel speeds -> body motion -> one integrated step. No I/O.
|-- sim.py # THE RIG: run a command sequence, return the trace
`-- main.py # the hand-checked cases, then the square and its error
Get the geometry provably right first. Every later robotics project reuses this integration step, so a quiet bias here follows you through the whole topic.
Write new_pose(x, y, theta) and wrap(theta) in pose.py, with the units, metres and radians,
stated in the docstring and nowhere else needing to repeat them.
Check: the units are written down. Mixing degrees and radians is the single most common bug in this project and it is invisible until the robot turns wrongly.
Write body_from_wheels(vL, vR, L) returning v = (vL + vR)/2 and w = (vR - vL)/L. Keep it a
pure function of three numbers returning two, with no pose and no state: this is the kinematics
alone, and separating it from the integration in the next step is what lets you check the three
cases by arithmetic. Equal wheels give w = 0 and v equal to either wheel; equal and opposite
wheels give v = 0 and a non-zero w; and one wheel stopped gives half the speed and a turn rate
set by L. Note that L divides only the turn rate, so a wrong wheelbase leaves straight lines
perfect and every rotation quietly scaled.
Check: equal wheel speeds give w = 0; opposite equal speeds give v = 0 and pure rotation. Those two
cases catch most sign and algebra errors immediately.
Write step(pose, vL, vR, dt, L): update theta first, then x and y using the new heading, every term
multiplied by dt.
Check: drive straight for a known time and the distance matches v x t exactly. A missing dt makes the
robot absurdly fast; the wrong order produces a small persistent drift.
Write run(commands, dt, L) looping step over a sequence of (vL, vR, duration) commands and
recording the pose. Default L=0.30 m for the wheelbase and dt=0.01 s for the integration step.
L is the only physical dimension in the whole project and it divides the turn rate, so getting it
wrong scales every rotation silently; dt is the number the edge-case step later sweeps, so keeping
it an argument rather than a constant is what makes that experiment one loop.
Check: a constant unequal-speed command traces a circle, and its radius matches v/w. That numeric
check is much stronger than "it looks curved".
Plot the trajectory and overlay the heading at intervals, as short arrows drawn from theta. Plotting
position alone would hide a whole class of bug: a pose whose heading has drifted out of step with its
motion still traces a perfectly plausible-looking curve.
Check: the heading arrows are tangent to the path. If they are not, theta and the position integration have drifted apart.
Write main.py to run the three cases whose answers you know without simulating: both wheels zero, wheels equal and
opposite, and both wheels negative. These are the cheapest possible tests and they catch the two
mistakes that otherwise survive into every later project. A sign error in omega and a missing dt
on one of the three integration lines.
Check: each behaves correctly, and rotation in place does not translate the robot.
Drive a shape, a square or a figure of eight, from a command sequence. Express the shape as a list
of (vL, vR, duration) commands rather than as code that computes wheel speeds, so the behaviour
is data you can print beside the resulting path. A square is four straight runs alternating with
four in-place turns of a quarter turn each, and the turn duration follows from w = (vR - vL) / L:
solve for the time that integrates to pi/2. The closing error when the path fails to return to its
start is your accumulated integration error, which is the number worth recording.
Check: the robot closes the shape and returns near its start. Accumulated error over a closed loop is a neat way to see integration drift, which is exactly the theme the odometry lesson raised.
Portfolio presentation. Lead with the plotted trajectories: a straight run, a circle with its measured radius against , and a closed shape showing the return error. Those numeric validations turn a drawing into evidence that the kinematics are right. State your units convention explicitly and mention the theta-before-position integration order. This is the foundation every later robot project builds on, so presenting it as verified rather than merely working is the point.
Full solutiontry the steps first - click to reveal
The complete simulator, one file at a time, then the results, including the one that is not what you expect. Work the steps first; the square in step 7 is where the integrator shows you its cost.
# pose.py - the pose, with its UNITS written down. Metres and radians, everywhere, always.
import math
def new_pose(x=0.0, y=0.0, theta=0.0):
"""(x, y) in METRES, theta in RADIANS measured from +X, CCW positive."""
return (x, y, theta)
def wrap(theta):
"""Keep theta in (-pi, pi]. Without this, a robot spinning for a minute reports a
heading of 40 radians, which is correct and unusable."""
return math.atan2(math.sin(theta), math.cos(theta))
# kinematics.py - wheel speeds to body motion, and the integration. No plotting, no I/O.
import math
from pose import wrap
def body_from_wheels(vL, vR, L):
"""v = average, w = difference / axle length. Two lines, and every differential-drive
robot in the course rests on them."""
return (vL + vR) / 2.0, (vR - vL) / L
def step(pose, vL, vR, dt, L=0.30):
"""Integrate one timestep. HEADING FIRST, then position with the NEW heading."""
x, y, th = pose
v, w = body_from_wheels(vL, vR, L)
th = wrap(th + w * dt) # ...first
x += v * math.cos(th) * dt # ...then position, using the new theta
y += v * math.sin(th) * dt
return (x, y, th)
# sim.py - run a command sequence and record the trajectory.
from kinematics import step
from pose import new_pose
def run(commands, dt=0.01, L=0.30, pose=None):
"""commands: a list of (vL, vR, duration_s). Returns the pose trace."""
p = pose or new_pose()
trace = [p]
for vL, vR, dur in commands:
for _ in range(int(round(dur / dt))):
p = step(p, vL, vR, dt, L)
trace.append(p)
return trace
# main.py - the three hand-computable cases, then a closed shape and what it costs.
import math
from sim import run
from kinematics import body_from_wheels
L = 0.30
W_SPIN = 1.0 / L # rad/s at vL=-0.5, vR=+0.5
if __name__ == "__main__":
# --- the three cases whose answers you know WITHOUT simulating -------------------
v, w = body_from_wheels(0.5, 0.5, L)
assert abs(v - 0.5) < 1e-12 and abs(w) < 1e-12, "equal wheels: pure translation"
v, w = body_from_wheels(-0.5, 0.5, L)
assert abs(v) < 1e-12 and abs(w - W_SPIN) < 1e-9, "opposite wheels: pure rotation"
v, w = body_from_wheels(0.0, 0.0, L)
assert v == 0.0 and w == 0.0, "stopped is stopped"
print("kinematics: 3 hand-checked cases OK")
# --- straight line: distance must equal v x t, EXACTLY ---------------------------
x, y, th = run([(0.5, 0.5, 4.0)])[-1]
assert abs(x - 2.0) < 1e-9, f"straight: x = {x}, expected 2.0"
assert abs(y) < 1e-12 and abs(th) < 1e-12, "a straight line must not drift"
print(f"straight: 4 s at 0.5 m/s -> x = {x:.6f} m, y = {y:.1e}, theta = {th:.1e}")
# --- spin in place: no translation at all ----------------------------------------
x, y, th = run([(-0.5, 0.5, (math.pi / 2) / W_SPIN)])[-1]
assert abs(x) < 1e-12 and abs(y) < 1e-12, "a spin must not translate"
print(f"spin: commanded 90.000 deg -> got {math.degrees(th):.3f} deg, "
f"x = {x:.1e}, y = {y:.1e}")
# --- an arc: curves toward the SLOWER wheel ---------------------------------------
x, y, th = run([(0.4, 0.5, 3.0)])[-1]
assert y > 0 and th > 0, "the right wheel faster must curve toward +y, the slower side"
print(f"arc: vL 0.4, vR 0.5, 3 s -> ({x:.3f}, {y:.3f}) m, "
f"{math.degrees(th):.1f} deg - curves toward the SLOWER wheel")
# --- the square, and what the integrator costs ------------------------------------
print("\nsquare: 4 x (1 m straight + 90 deg turn). It should return to the origin.")
print(" dt (s) closure error (m) final heading (deg)")
prev = None
for dt in (0.05, 0.01, 0.001, 0.0001):
x, y, th = run([(0.5, 0.5, 2.0), (-0.5, 0.5, (math.pi / 2) / W_SPIN)] * 4, dt=dt)[-1]
err = math.hypot(x, y)
print(f" {dt:<10} {err:>10.5f} {math.degrees(th):+8.3f}")
if prev is not None:
assert err < prev, "halving dt must reduce the error - the integrator converges"
prev = err
What it prints.
kinematics: 3 hand-checked cases OK
straight: 4 s at 0.5 m/s -> x = 2.000000 m, y = 0.0e+00, theta = 0.0e+00
spin: commanded 90.000 deg -> got 89.763 deg, x = 0.0e+00, y = 0.0e+00
arc: vL 0.4, vR 0.5, 3 s -> (1.135, 0.622) m, 57.3 deg - curves toward the SLOWER wheel
square: 4 x (1 m straight + 90 deg turn). It should return to the origin.
dt (s) closure error (m) final heading (deg)
0.05 0.20703 -16.225
0.01 0.01170 -0.946
0.001 0.00225 -0.183
0.0001 0.00037 -0.030
Steps 1 to 3: the pose, the conversion, the integration.
THE UNITS GO IN THE DOCSTRING, and this is not pedantry. Every mixed-unit bug in
robotics looks like a physics problem: degrees where radians were expected gives a robot
that turns 57 times too far, and the symptom is a robot spinning wildly rather than an
error message. Write "metres and radians" in `pose.py` and every other file inherits it.
THE WRAP IS THE OTHER HALF: without it, a robot spinning for a minute reports a heading
of 40 radians, which is arithmetically correct and useless to compare, plot or feed to
an atan2. Wrap once, in `pose.py`, so no caller has to remember.
HEADING FIRST, THEN POSITION WITH THE NEW HEADING. Update x and y with the OLD theta and
the error is small, systematic and in one direction - it shows up as a slow drift in y on
a straight line, which looks like a wheel-calibration problem and is not.
Steps 4 to 6: the three cases you can check without simulating.
| Case | Command | Expected | Measured | What it pins |
|---|---|---|---|---|
| Straight | 0.5, 0.5 for 4 s | x = 2.000 m, y = 0, theta = 0 | 2.000000, 0.0e+00, 0.0e+00 | The units: v x t, exactly |
| Spin | -0.5, +0.5 | x = y = 0, theta increases | 0.0e+00, 0.0e+00, 89.763 deg | The sign convention and that a spin does not translate |
| Arc | 0.4, 0.5 for 3 s | Curves toward +y | (1.135, 0.622), 57.3 deg | Which way it curves, toward the slower wheel |
THESE THREE BETWEEN THEM CATCH EVERY KINEMATICS BUG THIS PROJECT CAN HAVE, and it takes
all three: a sign error in w is completely invisible while the wheels match, and shows up
the instant they differ. The straight-line case pins the units, the spin pins the sign,
the arc pins the direction.
NOTE THE SPIN ALREADY SHOWS THE DISCRETISATION: 89.763 degrees against a commanded 90.
The turn takes 47.12 timesteps at dt = 0.01 and the loop runs a whole number of them, so
0.12 of a step is lost. That 0.24-degree error is not a bug - it is the cost of a
fixed timestep, and the next section is what it compounds into.
Step 7: the square, and the result worth having.
FOUR STRAIGHTS AND FOUR EXACT 90-DEGREE TURNS SHOULD RETURN TO THE ORIGIN. It does not,
and how much it misses by depends entirely on the timestep:
dt = 0.05 s 208 mm out, heading 16.2 degrees wrong <- a fifth of a metre
dt = 0.01 s 12 mm out, heading 0.95 degrees wrong
dt = 0.001 s 2 mm out, heading 0.18 degrees wrong
dt = 0.0001 s 0.4 mm out, heading 0.03 degrees wrong
THE ERROR IS ROUGHLY PROPORTIONAL TO dt, which is the signature of a first-order (Euler)
integrator. Halve the timestep and halve the error; there is no timestep at which it
becomes zero.
WHY IT ACCUMULATES: each turn is quantised to a whole number of steps, so each is
slightly short, and the heading error carries into the next straight - which then points
in the wrong direction for a whole metre. Four of those compound.
THIS IS EXACTLY WHAT ODOMETRY DRIFT IS. A real robot integrating wheel encoders has this
same error plus wheel slip, plus a wheel-radius estimate that is a little wrong, plus an
axle length that is a little wrong - and it compounds in the same way, for the same
reason. A robot that has driven a hundred metres does not know where it is, and this
table is why, in a system with no sensor noise at all.
THE CONCLUSION TO WRITE DOWN: dead reckoning is a short-term tool. It is excellent over
seconds and useless over minutes, and every localisation method later in this topic
exists to correct it with something absolute.
Step 8: the shape as a command sequence.
The square is written as [(straight), (turn)] * 4. Two commands and a multiplication, because the
turn duration is computed from the geometry ((pi/2) / w) rather than typed as a number. Change the
axle length L and the turn duration follows automatically; type 0.4712 and it does not, and the
square stops closing for a reason that has nothing to do with the integrator.
Seeing it in 3D, on Windows or macOS, without installing Linux. Nothing above needs a simulator, and that is deliberate: building the rig yourself is what keeps the kinematics visible and testable. But the Gazebo work later in this course does assume Linux, and if you want a robot moving in three dimensions under your own controller before you commit to that, Webots is the shortest route. It is Apache-2.0, it installs natively on Windows, macOS and Linux, and it ships with working robots, so the distance from download to something driving under your code is one sitting rather than one weekend of setup.
The exercise worth doing is not "look at a nicer robot". Open a built-in differential-drive robot,
drive it with the same command sequence you used in step 7, and compare the closing error of the
square against the one your own sim.py produced. Yours integrates the motion model you chose;
Webots integrates a physics engine with wheel contact, slip and mass. The gap between the two
numbers is the reality gap in miniature, met on a program you understand completely, which is a far
better first encounter with it than meeting it on hardware.
Keep the proportion right. Webots is the on-ramp and the prototyping tool; Gazebo is what ROS 2 jobs expect, and the simulators hands-on guide in Turn 2 sets out when to reach for which. A screenshot is not a substitute for the plotted trajectories and their measured errors, which stay the evidence this project is judged on.
Why it exists. The Pass-1 lessons gave you the math of frames, kinematics, and motion models, but math becomes real understanding only when you build something that uses it. A differential-drive simulator is the ideal first build: it requires exactly those concepts (a pose in a frame, kinematics to get velocity, a motion model to integrate it, visualisation to see it), it's pure software (cheap, safe, fully observable: the simulate-first practice), and it produces a visible, verifiable result. It cements the foundations and gives you a tool (and a portfolio piece) you'll reuse, which is why it's the first portfolio project.
Mental model. The simulator is like a flight simulator for a tiny two-wheeled robot, drawn on graph paper. You hold the robot's situation as a dot with an arrow on the graph paper (its pose, where it is and which way it faces). Each tick of the clock, you read how fast each wheel is turning, work out from that how fast the whole robot is going forward and how fast it's turning (the kinematics), then nudge the dot forward and rotate the arrow by that much (the motion model) and leave a breadcrumb. Do that many times a second and the breadcrumbs trace the robot's path. There's no real robot, no risk, no cost: just the rules of how a two-wheeled robot moves, applied over and over on graph paper, letting you watch the math you learned actually drive a robot around.
Common misunderstandings.
- "A simulator needs a physics engine / it's complicated." A basic differential-drive simulator is just the kinematics + motion-model integration in a loop. A pose updated each timestep from wheel speeds. You don't need a full physics engine to capture the kinematic motion; the simple integration is the simulator (more realism, like noise and slip, is added later).
- "Bigger timesteps are fine." Integrating the motion model with too large a dt introduces error (the robot
moves in straight-line chunks each step, approximating curves coarsely). Smaller
dtgives a more accurate trajectory. A reminder that the motion model is a discrete approximation of continuous motion. - "The simulator shows the true position, so there's no drift." The simulator's integration is the ground truth here, but it models exactly what odometry does, so it lets you see that if you fed in noisy/slipping wheel measurements, the integrated estimate would drift from truth. The project is the place to make odometry's drift visible.
Connections. This project makes Pass-1 concrete: it directly implements differential-drive kinematics (the differential-drive kinematics lesson) and the motion model (the odometry lesson), uses coordinate frames (coordinate-frames 08) for the pose, and applies simulate-first (the sim/test/debug lesson). It's the robotics-framed sibling of the C++ diff-drive project and uses the Python/visualisation skills from that topic. It's the foundation for the next project (the explorer adds sensing, mapping, and decision-making on top of this motion core) and a stepping stone to the full Gazebo simulations, your first runnable robot.
BImmediate Active Recall
QUERYWhat state does the simulator hold, and what happens each timestep?
REVEAL
It holds the robot's pose (x, y, ), its position and heading in the world frame. Each timestep dt: (1) take the commanded wheel speeds (vL, vR); (2) apply the differential-drive kinematics to get the robot's velocity: and ; (3) apply the motion model to update the pose (integrate x += vdtcos(theta); y += vdtsin(theta); theta += omega*dt); (4) visualise: draw the robot and append the new position to its trajectory. Repeated many times a second, this loop moves the robot and traces its path. Exactly the integration real odometry performs.
QUERYWhich Pass-1 concepts does building the simulator bring together?
REVEAL
Coordinate frames (the coordinate-frames lesson). The robot's pose lives in the world frame; differential-drive kinematics (the differential-drive kinematics lesson) (converting wheel speeds to (v, ); the motion model (the odometry lesson)) integrating the velocity to update the pose each step (the same thing odometry does); and visualisation to see and verify the result. So the project ties together frames + kinematics + motion model + visualisation. The core geometry and motion of Pass 1. Into one running program.
QUERYHow do you verify the simulator against the kinematics you learned?
REVEAL
Drive the wheels in the canonical patterns and check the motion matches: equal wheel speeds -> a straight line (), unequal speeds -> an arc (the tighter the speed difference, the tighter the turn: radius ), and equal and opposite speeds -> spinning in place (). If the simulated trajectory shows exactly these behaviours, the kinematics and motion-model integration are correct. It's a direct, visible check that your implementation of , , and the pose integration is right. The value of visualising to verify.
QUERYWhy is a simulator the natural first robotics project?
REVEAL
Because it requires exactly the Pass-1 foundations (a pose in a frame, kinematics, a motion model, visualisation) and so cements them by making you build something that uses them; it's pure software: cheap, safe, and fully observable (the simulate-first practice, no hardware to break, you can see every value); and it produces a visible, verifiable result (the trajectory) you can check against the math. It turns abstract geometry/motion into a runnable, checkable tool (and a portfolio piece), and it's the motion core that later projects (the explorer, Gazebo sims) build sensing and decision-making on top of, the ideal first build.
CConceptual Questions
Answer each in your own words in the box, then reveal the model answer to compare. These ask why, not how, and your answers are saved.
Why does building a differential-drive simulator cement the Pass-1 foundations in a way that studying the math alone cannot, and what does this say about learning robotics by building?
REVEAL MODEL ANSWER
Building a differential-drive simulator cements the Pass-1 foundations in a way studying the math alone cannot because it forces you to make the concepts precise, complete, and correct enough to actually run: turning passive familiarity into active, working understanding, and exposing any gaps that reading glosses over. When you study the kinematics and motion model on paper, you can follow the equations and feel you understand them, but that understanding is often partial: you may not have pinned down exactly what the state is, exactly how the pieces connect, or exactly how the continuous motion becomes a discrete update. Building a simulator removes all vagueness, because the computer demands precision: you must decide that the state is the pose (x, y, ), decide it's in the world frame, write the kinematics (, ) correctly, write the integration (x += vdtcos(theta), etc.) with the right trig and the right dt, and connect them in the right order, and if any of it is wrong or missing, the simulated robot moves visibly wrong (drives sideways, curves the wrong way, spins when it shouldn't). So building tests your understanding against an unforgiving check: the robot either moves correctly or it doesn't, and making it correct requires genuinely understanding how the pieces fit together, not just recognising them. This is why it cements the foundations: it converts the separate lessons (frames, kinematics, motion model, visualisation) into a single working mechanism you constructed and verified, which you now understand operationally. You know how a wheel command becomes a velocity becomes a pose update becomes a drawn trajectory, because you wired that chain yourself and watched it work. It also makes the abstract concrete and visible: you see equal wheel speeds produce a straight line and opposite speeds produce a spin, so the kinematics stop being formulas and become intuitions about how a robot moves. And it surfaces subtleties that reading hides: that the motion model is a discrete approximation (so dt matters), that the pose must be in a consistent frame, that the integration order matters. Learnings you only get by making it actually run. What this says about learning robotics by building is a central principle of the whole course: you learn robotics by building robots (and simulators of them), because robotics is an engineering discipline where understanding must be operational: precise and complete enough to make a real (or simulated) system work. Reading and math give you the concepts; building forces you to integrate them into a correct, working whole, which is a far deeper and more durable form of understanding, and it's exactly the kind of understanding a roboticist needs, since the job is to make real systems work, not to recite equations. The simulator is the first instance of this build-to-understand pattern that recurs through every project: each build takes concepts and makes you realise them in something that runs, cementing them through the discipline of making them actually work. That's why the topic is structured around portfolio projects, and why this simulator (simple as it is) teaches the foundations more deeply than any amount of studying the equations in isolation.
Why is starting with a pure-software simulator (rather than hardware) the right first project, and how does this embody the simulate-first, observable-and-safe testing philosophy?
REVEAL MODEL ANSWER
Starting with a pure-software simulator rather than hardware is the right first project because it gives you all the learning value of building a working robot motion system while removing all the cost, risk, and opacity of hardware, which is exactly the simulate-first philosophy, and exactly what a first project should be. Consider what you want from a first build: to implement and genuinely understand the core motion concepts (kinematics, motion model, frames), to get a working, verifiable result, and to be able to experiment freely and learn from mistakes. A pure-software simulator delivers all of this optimally. It's cheap: no robot to buy, no parts to break, no lab to set up. Just code, so you can build it now and run it endlessly for free. It's safe: a simulated robot can drive into walls, spin wildly, or behave completely wrong with zero consequences, so you can experiment boldly, deliberately try things, and make mistakes freely, which is how you learn (on hardware, a bug might break the robot or take hours to reset). It's fully observable: in software you can see every value (the exact pose each step, the computed v and , the whole trajectory) so when something is wrong you can inspect precisely what happened, and when it's right you can verify it exactly against the math; on hardware, the robot's internal state is hidden and its motion is affected by noise and physics you can't see, making it far harder to tell whether your understanding (the kinematics/motion model) is correct or whether some physical effect intervened. And it's fast and repeatable: you can run the same scenario instantly and as often as you like, tweak and re-run, iterate rapidly. This is precisely the simulate-first, observable-and-safe testing philosophy from the testing lesson: prove the logic in a cheap, safe, observable, repeatable simulation before facing the expensive, risky, opaque reality of hardware. By starting with the simulator, you isolate the conceptual core (does my kinematics-and-motion-model logic correctly produce the right motion?) in an environment where you can verify it cleanly (so you know your understanding and code are right) before ever adding the complications of real sensors, real motors, slip, and noise. It embodies the philosophy perfectly: the simulator is the rehearsal room where you get the fundamentals provably correct, cheaply and safely, with full visibility. It also respects the reality gap honestly: the simulator validates the kinematic logic, and you understand that real hardware will add effects (noise, slip) the sim doesn't, but those are best tackled after the clean logic is solid, exactly the staged approach simulate-first prescribes. So a pure-software simulator is the ideal first project because it maximises learning (you build and verify the real motion concepts) while minimising cost and risk and maximising observability: the simulate-first principle made into a concrete first build, teaching you both the foundations and the right way to develop robot software: prove it in simulation first, where it's cheap, safe, and you can see everything.
DPractice Problems
P1 (easy). Write the simulator's core and run its three verification cases.
Give step(pose, vL, vR, dt, L) and a run helper, then produce the results table for: straight (vL = vR = 0.5 m/s for 2 s), arc (vL = 0.4, vR = 0.6, one full circle), and spin (vL = -0.5, vR = 0.5 for 1 s), with L = 0.30 m and dt = 1 ms. State the hand-computed prediction for each and the measured result, and explain the one discrepancy.
P2 (medium). Build the test suite that catches sign and scale errors. Write it as three asserts on the three cases, then deliberately introduce three bugs and produce the table of what each verification case reports for each bug:
- (a sign slip in the linear velocity),
- (the angular sign reversed),
- (half the wheelbase used).
Say which case catches which bug, whether any bug survives all three, and what that says about choosing test cases.
P3 (harder). Quantify what the simulator buys and what it cannot.
Time 200 full five-second runs at dt = 1 ms in the simulator, and compare against the same 200 runs on hardware allowing 30 s of setup each. Then write the parameter sweep the simulator makes possible (for example, finding the wheelbase that best explains a recorded trajectory) as code. Finally, list four things about this robot that the simulator cannot tell you, and state the rule for what belongs in simulation and what does not.
Solutionsclick to reveal
P1. The core.
import math
def step(pose, vL, vR, dt, L=0.30):
x, y, theta = pose
v = (vR + vL) / 2.0
omega = (vR - vL) / L
theta += omega * dt # heading first
x += v * math.cos(theta) * dt
y += v * math.sin(theta) * dt
return (x, y, theta)
def run(vL, vR, duration, dt=0.001, L=0.30, pose=(0.0, 0.0, 0.0)):
for _ in range(int(duration / dt)):
pose = step(pose, vL, vR, dt, L)
return pose
The three cases: prediction first, then measurement.
| Case | Hand prediction | Measured |
|---|---|---|
| Straight vL = vR = 0.5, 2 s | , , so , , | x = 1.0000, , |
| Spin vL = -0.5, vR = 0.5, 1 s | , , so x = , | , , = 3.3333 |
| Arc vL = 0.4, vR = 0.6 | , , 0.7500 m, 9.425 s | after 9.425 s: x = -0.0004, , = 6.2827 |
Predicting before running is the entire method, and it is what makes the simulator a check on your understanding rather than a picture of it. A plot that "looks like a circle" is consistent with a dozen wrong implementations; a radius of 0.7500 m is consistent with one.
Where each prediction comes from.
- Straight: equal wheels, so and the heading never changes; .
- Spin: equal and opposite, so and the position cannot change; .
- Arc: , the definition of the turning radius, and one circuit takes seconds.
The one discrepancy: the arc does not close exactly. It returns to (-0.0004, 0.0000) instead of (0, 0), and the heading reaches 6.2827 instead of 2 pi = 6.2832.
That is not a bug. It is the integration error of a first-order (Euler) method, and it is worth confirming that it behaves as first-order error should:
| dt | Closing error |
|---|---|
| 100 ms | 0.0124 m |
| 10 ms | 0.0024 m |
| 1 ms | 0.0004 m |
Ten times smaller dt gives roughly ten times smaller error, which is the signature of a first-order method: error proportional to dt. If halving dt did not roughly halve the error, something other than integration error would be present, and that is the test that distinguishes "my integrator is approximate" from "my equations are wrong".
Two ways to do better if you need to, both worth knowing and neither necessary here: integrate at the midpoint heading (), which is second order and reduces this error by about a thousandfold at the same dt; or use the exact arc update, which for constant and has a closed form and closes the circle to machine precision. Euler with a small dt is the right default because it is the same arithmetic the robot's own odometry does, so the simulator's error matches the robot's.
P2. The suite.
# tests/test_kinematics.py
import math
from sim import run
L = 0.30
def test_straight():
x, y, theta = run(0.5, 0.5, 2.0, L=L)
assert abs(x - 1.0) < 1e-6
assert abs(y) < 1e-9
assert abs(theta) < 1e-9
def test_spin_in_place():
x, y, theta = run(-0.5, 0.5, 1.0, L=L)
assert abs(x) < 1e-9 and abs(y) < 1e-9
assert abs(theta - 1.0 / L) < 1e-6 # +3.3333 rad/s for 1 s. Note the SIGN
def test_arc_radius():
vL, vR = 0.4, 0.6
v, omega = (vR + vL) / 2, (vR - vL) / L
assert abs(v / omega - 0.75) < 1e-9 # the radius, computed from the model
x, y, theta = run(vL, vR, 2 * math.pi / omega, L=L)
assert math.hypot(x, y) < 0.01 # the circle closes
The bug table.
| Implementation | Straight (0.5, 0.5, 2 s) | Arc radius | Spin |
|---|---|---|---|
| correct | , , | 0.7500 m | +3.3333 |
1. v = (vR - vL)/2 |
0.1500 m | +3.3333 | |
2. omega = (vL - vR)/L |
, | -0.7500 m | -3.3333 |
3. omega over L/2 |
, | 0.3750 m | +6.6667 |
Which case catches which.
Bug 1 is caught by the straight test, immediately and unmistakably. With equal wheels, is zero, so the robot does not move at all: where 1.0 was expected. It is also caught by the arc test (radius 0.15 m instead of 0.75), but the straight test's failure is the clearest: a robot commanded forward that stays put.
Bug 2 is invisible to the straight test. With equal wheels is zero either way, so the sign of a zero cannot be wrong. The spin test catches it instantly: where +3.3333 was expected, which on a real robot is the classic "it turns the wrong way" symptom. The arc test catches it too, as a negative radius.
Bug 3 is invisible to both the straight test and to any test of direction. The robot goes straight when told to, turns the right way when told to, and every sign is correct. Only a test of magnitude finds it: the spin rate is exactly double, and the arc radius exactly half.
Does any bug survive all three? No, and the reason is that the three cases exercise different parts of the model:
| Case | What it exercises |
|---|---|
| Straight | the linear term, with the angular term forced to zero |
| Spin | the angular term, with the linear term forced to zero |
| Arc | both together, and the relationship between them |
The first two isolate one term each; the third tests that they combine correctly. A bug in either term alone is caught by the corresponding isolating test; a bug in how they interact would show only in the arc, which is why all three are needed and why two of them are not enough.
What this says about choosing test cases.
Isolate, then combine. Two of the three cases deliberately force one term to zero. That is what makes their failures diagnostic: "the straight test failed" points at , not at the whole model. A suite made only of realistic mixed motions tells you something is wrong and not what.
Every test case must have a hand-computed expected value. 0.7500 m and were computed from the formulas before any code ran. A test that asserts what the code currently does is a regression detector, not a correctness check, and it would have passed happily on all three buggy versions.
Beware the symmetric case. Bug 2, the sign reversal, is undetectable in the straight test precisely because that case is symmetric in the two wheels. A test case that is symmetric cannot detect a bug that is symmetric, which is the same trap as testing a rotation at 0 degrees or a fuser with two equal variances. At least one case must break every symmetry, and here the spin case is the one that does.
P3. The timing.
| 200 runs of 5 simulated seconds | |
|---|---|
| Simulator, dt = 1 ms | 0.57 s of wall time (2.8 ms per run) |
| Hardware, 5 s each plus 30 s of setup | 1.9 hours |
Twelve thousand times faster, and that ratio is the whole argument. It is not a convenience: it changes which questions are worth asking. A sweep that costs two hours gets done once, carefully, with a hypothesis. A sweep that costs half a second gets done whenever you are curious, which is how the interesting result is usually found.
Four properties the simulator has that the hardware cannot.
It is deterministic, so a failure is reproducible exactly. It is instrumented completely: the true pose is available at every step, which on hardware would require a motion-capture rig. It cannot break, so a gain of 500 or a wheelbase of 0.01 m costs nothing. And it can run faster than real time, which is what makes the sweep below possible.
The parameter sweep.
# calibrate.py - find the wheelbase that best explains a recorded run
import math
from sim import run
def closing_error(L, vL, vR, duration, recorded_end):
x, y, theta = run(vL, vR, duration, dt=0.001, L=L)
return math.hypot(x - recorded_end[0], y - recorded_end[1])
def best_wheelbase(vL, vR, duration, recorded_end, lo=0.20, hi=0.40, n=201):
candidates = [lo + (hi - lo) * i / (n - 1) for i in range(n)]
return min(candidates, key=lambda L: closing_error(L, vL, vR, duration, recorded_end))
L_est = best_wheelbase(0.4, 0.6, 9.425, recorded_end=(0.03, -0.11))
Two hundred candidate wheelbases evaluated in about half a second, against a single recorded trajectory from the real robot. This is genuine calibration, and it is the thing a simulator is most useful for that is not obvious in advance: the model's parameters are fitted to the hardware, after which the simulator predicts the hardware rather than merely resembling it.
The same loop answers other questions at the same cost: which dt is small enough, how much a 1 percent wheel mismatch costs over a 50 m route, what the closing error is for every plausible (, ) pair.
Four things this simulator cannot tell you.
Wheel slip. The model assumes the wheels roll without slipping, so the simulator's robot turns exactly as commanded on ice, on carpet and on a wet floor. Slip is the single largest source of real odometry error and the model has no representation of it.
Actuator limits and dynamics. The wheels reach 0.5 m/s instantly, with no acceleration limit, no torque limit, no motor time constant and no deadband. A real robot commanded 0 to 0.5 m/s takes a few hundred milliseconds and the difference shows up on every start and stop.
Sensor and timing reality. No encoder quantisation, no sensor noise, no latency, no loop jitter, and dt is exactly 1 ms every single step. On hardware dt varies, and the previous lessons showed what that does to a derivative.
Everything mechanical. Backlash, a loose grub screw, a wheel that is 0.3 mm smaller than its twin, a caster that judders, a cable that snags at full lock. None of these exist in three lines of kinematics, and all of them exist on the robot.
The rule for what belongs in simulation.
Simulate to validate logic and to explore parameters; test on hardware to validate physics and interfaces.
| Question | Where it is answered |
|---|---|
| Does the planner find a route? Is the sign convention right? Is the state machine correct? | Simulation. It is deterministic, fast and safe |
What wheelbase best fits the recorded run? What dt is enough? |
Simulation, by sweeping |
| Does it slip on that ramp? Does the gripper actually close on the tote? Is the latency acceptable? | Hardware. Nothing else can answer it |
| Does it still work after eight hours? | Hardware, always |
And the reason to simulate first is not that simulation answers everything: it is that it removes every question it can answer, so the expensive hardware time is spent on the questions only hardware can answer. A robot that arrives at its first hardware test with the kinematics, the state machine and the planner already verified fails for physical reasons, which are the reasons worth spending hardware time on.
EFeynman Exercise
Explain to a beginner, using the picture of a flight simulator for a tiny two-wheeled robot drawn on graph paper: (1) why you hold the robot as a dot-with-an-arrow (its pose) and, each tick, work out from the wheel speeds how fast it goes forward and turns (the kinematics), (2) why you then nudge the dot and rotate the arrow by that much and leave a breadcrumb (the motion model, integrated step by step), and (3) why doing this on graph paper (with no real robot, risk, or cost) lets you watch the math you learned actually drive a robot around.
REVEAL MODEL ANSWER
The differential-drive simulator is best pictured as a flight simulator for a tiny two-wheeled robot, drawn on graph paper. First, you hold the robot as a dot-with-an-arrow (its pose) and, each tick, work out from the wheel speeds how fast it goes forward and turns. The dot is where the robot is on the graph paper, and the arrow is which way it faces: together that's its pose (x, y, and heading). Each tick of the clock, you look at how fast each wheel is spinning, and use the simple rules from the kinematics lesson: the average of the two wheel speeds is how fast it goes forward, the difference is how fast it turns: to get the robot's forward speed and turn rate. Second, you then nudge the dot and rotate the arrow by that much, and leave a breadcrumb. Knowing the forward speed and turn rate for this tick, you move the dot forward (in the direction the arrow points) and rotate the arrow by the turn. Just a little, because it's only one tick. That little update is the motion model, and you do it over and over, every tick, dropping a breadcrumb at each new position. String the breadcrumbs together and you've drawn the robot's whole path: a straight line if both wheels match, a curve if they differ, a tight spin if they're opposite. Third, doing this on graph paper: with no real robot, risk, or cost: lets you watch the math you learned actually drive a robot around. There's no hardware to buy or break, nothing dangerous, and you can see every step: so you can run it again and again, try different wheel speeds, and watch the kinematics and motion model you studied come alive as a robot tracing paths on the page. It's the cheapest, safest, clearest way to make the abstract motion math real: just the rules of how a two-wheeled robot moves, applied over and over on graph paper, turning equations into a little robot you can drive. (And because the simulator is doing exactly what odometry does, you can even feed it slightly-off wheel readings and watch the estimated path drift. Seeing odometry's flaw with your own eyes.)
FError Analysis Framework
- Thinking a basic simulator needs a full physics engine. Why: simulating motion sounds complex. Recognise: the kinematics + motion-model integration in a loop IS a basic diff-drive simulator. Avoid: implement the simple pose update from wheel speeds; add realism (noise/slip/physics) later.
- Using too large a timestep dt in the integration. Why: a big dt runs faster. Recognise: large dt makes the motion model (a discrete approximation) inaccurate (coarse straight-line chunks). Avoid: use a small enough dt for an accurate trajectory; remember it approximates continuous motion.
- Getting the pose-update trig or frame wrong. Why: just add the motion to x and y. Recognise: motion is along the current heading. Needs in a consistent frame, in the right order. Avoid: integrate x += vdtcos(theta); y += vdtsin(theta); theta += omega*dt, all in the world frame.
- Assuming the simulator shows there's no odometry drift. Why: the simulated position is exact. Recognise: the sim is ground truth, but it models exactly what odometry integrates. Avoid: feed in noisy/slipping wheel readings to make odometry's drift visible (the point of the exercise).
GMini Challenge
Design and explain a differential-drive simulator project for a new roboticist: the state (pose), the per-timestep loop (wheel speeds -> kinematics -> motion-model integration -> visualise), how to verify it, and why building it (in pure software) cements the Pass-1 foundations and embodies simulate-first.
REVEAL MODEL ANSWER
State: the robot's pose (x, y, ) in the world frame.
Per-timestep loop (dt): (1) take commanded wheel speeds (vL, vR); (2) kinematics -> , ; (3) motion model -> integrate the pose: x += vdtcos(theta); y += vdtsin(theta); theta += omega*dt; (4) visualise -> draw the robot and append to the trajectory. Repeated each tick, it moves the robot and traces its path. Exactly what odometry integrates.
Verify: drive the canonical patterns: equal wheels -> straight (), unequal -> arc (), equal & opposite -> spin in place (). The trajectory matching these confirms the kinematics + integration are correct (visualise to verify).
Skills it brings together: coordinate frames (the pose, the coordinate-frames lesson), differential-drive kinematics (wheels -> v,, the differential-drive kinematics lesson), the motion model (integrate the pose, the odometry lesson), and visualisation: the Pass-1 geometry/motion lessons, running.
Why building it (in pure software) cements the foundations: building forces the concepts to be precise, complete, and correct enough to run: the computer demands you decide the state, write the kinematics and integration correctly, and connect them in order, or the robot moves visibly wrong. So it tests your understanding (it moves right or it doesn't), converts the separate lessons into one working mechanism you built and verified, makes the math concrete/visible (see equal->straight, opposite->spin), and surfaces subtleties (dt matters: the motion model is a discrete approximation; the pose must be in a consistent frame). This is the build-to-understand pattern: robotics understanding must be operational (precise enough to make a system work), so building teaches the foundations more deeply than studying the equations.
Why pure software / simulate-first: it's cheap (just code), safe (crash with zero consequences: experiment and make mistakes freely), fully observable (see every value: verify exactly against the math, unlike hidden, noise-muddied hardware), and fast/repeatable. It isolates the conceptual core (is my kinematics+motion-model logic right?) and verifies it cleanly before adding real sensors/motors/slip/noise. The rehearsal room where you get the fundamentals provably correct, respecting the reality gap (validate the kinematic logic now; real effects later).
Connections: the robotics-framed sibling of the C++ diff-drive project (using Python/visualisation skills); the motion core the next project (the explorer: adds sensing, mapping, decision-making) builds on; and a stepping stone to full Gazebo simulation. Your first runnable robot.
Quiz Check
A quick auto-graded check, separate from the recall cards above. Your score feeds the dashboard Mastery metric. On a project it is optional. Working through the build walkthrough in section A is what completes this module.