Docs menu
No-ROS simulator
A headless F1TENTH Gym harness that calls gap_follow's and pure_pursuit's mathematics directly, with this car's parameters and collision detection that works.
tools/f1tenth_sim/ runs gap_follow and pure_pursuit on simulated tracks without ROS. It calls the same ROS-free modules the nodes use, gap_logic and racing_math, so it tests the mathematics that runs on the car. It is headless and deterministic, and needs no display, Docker or ROS topics.
The upstream F1TENTH Gym checkout stays unmodified. The workspace’s changes live beside it in tools/f1tenth_sim/sim_fidelity/GitHub and wrap it from outside.
Set up once
cd ~/racerbot-ws
tools/f1tenth_sim/setup.shIt clones f1tenth/f1tenth_gym at a pinned commit into .sim/, which Git ignores, installs pinned Python dependencies there, downloads the Spielberg track and runs a smoke test. It needs no sudo and no global pip install, and refuses to run if the checkout has local changes.
Run it
The simulator models one car’s calibration, so choose the car first. On a car’s Jetson it is found from the serial number. On a development computer, set it in the terminal, or the tools refuse to start with unknown car:
export RACERBOT_CAR=rb1| Task | Command |
|---|---|
| The full suite: three circuits and a two-car race | python3 tools/f1tenth_sim/run_validation.py --scenario all |
| A quick run on one track | python3 tools/f1tenth_sim/run_validation.py --scenario all --quick |
gap_follow on the indoor layouts |
python3 tools/f1tenth_sim/sweep_gap.py --track indoor --seeds 6 |
Several gap_follow cars, opponent awareness on |
python3 tools/f1tenth_sim/eval_gap_multi.py --track indoor_wide --cars 3 --speeds 1.8,3.5,3.5 --set enable_opponent_awareness=true |
| Compare steering laws | python3 tools/f1tenth_sim/law_sweep.py --laws stanley lqr mpc --seed 12345 --repeat-seeds 5 |
| Print every car parameter and its source | python3 tools/f1tenth_sim/sim_fidelity/calibration.py |
run_validation.py exits 0 only when every selected scenario passes, so it can gate a change. Each sweep line reads pass N/N when every run finished, with contact 0 for one car or wall 0 car-car 0 for several.
What the fidelity layer adds
Stock F1TENTH Gym models a different car, and its own collision check never fired in this harness’s original geometry: a car driven straight at a barrier at 4 m/s travelled 198.9 m through the circuit unflagged. The fidelity layer replaces what matters for this car.
| Module | Adds |
|---|---|
calibration.py |
This car’s parameters, each tagged with its source |
grip.py |
A friction circle, which the stock tire model lacks |
actuation.py |
A steering servo that settles, and command transport delay |
sensing.py |
Pose, odometry and scan error |
plant.py |
Wraps each simulation step, and checks collisions itself against the real chassis and the map |
Neither simulator takes its pass or fail from gym’s own collision flag. When gym’s flag fires, gym zeroes the car’s yaw, which once turned two simulated cars round to lap backwards, so the plant and car profiles switch that response off.
The friction circle
F1TENTH Gym models the car as a single-track (“bicycle”) vehicle with a linear tyre: the sideways force is proportional to the tyre’s slip angle, , with no upper limit. A real tyre saturates. Measured on this checkout, the stock model held 17.74 m/s², 1.81 g, at full lock and never levelled off, far beyond what a car on a lab floor can do. Braking also cost no cornering grip.
The upstream dynamics are compiled code the workspace keeps unmodified, so grip.py imposes the limit from outside, by limiting the steering angle the plant receives. A tyre can supply a total force of at most , the friction coefficient times the load, in any direction. Shared between braking and cornering, that is a circle:
At steady state the bicycle model turns on a radius , so its lateral acceleration is . Keeping that within the budget caps the steering:
def lateral_budget(self, longitudinal_accel):
limit = self.friction_accel_limit
used = longitudinal_accel * self.longitudinal_coupling
remaining = limit * limit - used * used
return math.sqrt(remaining) if remaining > 0.0 else 0.0
def max_steering(self, speed, lateral_budget):
if speed < KINEMATIC_SPEED:
return math.inf
return math.atan(self.wheelbase * lateral_budget / (speed * speed))This reproduces the envelope, so the simulated car can no longer corner harder than the floor allows, and hard braking now costs cornering. It does not reproduce a slide: at the limit the car runs wide along the envelope instead of spinning. Below 0.5 m/s the gym model switches to a kinematic bicycle with no slip at all, so no limit is applied there. Each run counts the ticks on which the limit acted, so a result that depended on it is visible.
--fidelity profile |
What it is |
|---|---|
legacy |
The original harness, bit for bit, so old results stay comparable |
plant |
The vehicle fixes with perfect sensing, to separate physics from sensing |
car |
Everything. The default. |
How far to trust it
Every value in calibration.py is tagged MEASURED, DERIVED, SPEC, ESTIMATED or STOCK. The actuation lags, servo gain, speed loop and floor friction were fitted to a real run on October 20, 2026. Six of 32 values are still F1TENTH Gym’s stock numbers for a different vehicle: mass, yaw inertia, centre-of-gravity height and position, and both cornering stiffnesses.
- The simulator models the selected car’s servo map and full-lock limits, read from its profile, so
RACERBOT_CARor--carmust name the car. Car 1’s right full lock is computed from its servo map, not measured. run_validation.pyre-implementspure_pursuit_node’s command stage. Since October 19, 2026, it reads its limits frompure_pursuit.yamlinstead of copying them, andgap_follow’s copy,gap_mirror.py, is held to the node by a test.- A simulator pass is not physical sign-off. Every change still follows the test order on the car.
The audit that measured the stock simulator against the car is docs/sim-fidelity-audit.mdGitHub, and the fidelity layer’s own account is tools/f1tenth_sim/README.mdGitHub.
Checked against
car 1 commit 0265263
and
car 2 commit b81b291, before the two workspaces merged.