Guilhem Carmouze

TLSe Racing, Formula Student driverless team. 2025 to 2026, with a follow-up in October 2026.

The simulation layer of a driverless race car

I wrote the 2D simulator, with its field-of-view sensor model, in which driving logic can be tried before anything runs on a car. In October 2026 I closed the loop: a controller that drives laps while seeing only what that sensor lets it see. All of it is 2D simulation.

Role
Member of the driverless team. Author of the simulation and tooling layer (November 2025), then of the closed loop, the benchmarks, the tests and the CI (October 2026).
Team
One teammate wrote the first reactive controller and the midpoint planner, another the RRT* planners and the smoothing.
Period
November and December 2025, then October 2026
Organisation
TLSe Racing, Formula Student driverless team. The two repositories are team prototypes hosted on my GitHub account, not the team’s official software.
Stack
Python, Pygame (pygame-ce), NumPy, SciPy, pytest, continuous integration, ffmpeg for the clips
One lap of the belgium track driven by the closed loop with the default sensor (4 m range, 100° field of view), played at twice the simulated speed. Left: the whole track. Right: a follow view. Dimmed cones are unknown to the car, cones in full colour are in its memory, cones with a white ring are inside the sensor sector right now. The red line is the centre line built from the blue and yellow pairs. 2D simulation, recorded off-screen by a script of the repository.

In three lines

Problem
The driverless team needed a place to try out driving logic before anything runs on a car, with a sensor that only shows the cones in front of the car.
What I built
A 2D Pygame simulator with a fit-to-track camera, a typed CSV track loader and a configurable field-of-view sensor model (2025), then a closed loop with cone memory, pure pursuit and a referee, and a benchmark of my teammates’ planners (October 2026).
Result
With the default sensor the car completes three valid laps on each of the four bundled tracks and touches no cone on three of them; on the hairpin track it touches 11 per lap. 2D simulation: no lap time here predicts a car.

Context

In the 2025 to 2026 season I was a member of the driverless team of TLSe Racing, a Formula Student team. In November 2025 two of its members started a small simulator in Python and Pygame to try out driving logic before anything runs on a car: I wrote the simulation layer, a teammate the first reactive controller.

Two repositories came out of it, both team prototypes hosted on my account. TLSe_Racing_Driverless is the simulator. PathPlanning (November and December 2025) holds three offline planners that build a closed reference line around a cone track, and a viewer that drives a car along it. The planning code is my teammates’ work. The simulator, the viewer, the camera and the track loader are mine.

In October 2026 I came back to both with what they lacked: measurements. In the simulator, a controller that completes laps from what the sensor sees. In PathPlanning, a command line, measures of the planned lines, a benchmark with committed results, tests and CI. My teammates’ code is left as they wrote it.

What I built

  1. November 2025

    The simulation layer

    A 2D Pygame simulator for Formula Student cone tracks: a typed CSV loader with a schema check, a camera that fits the track and zooms about the cursor, a car drawn at Formula Student scale, and a configurable field-of-view sensor model, a range and an opening angle, that selects the cones the car can see. The sensor model is purely geometric: no occlusion, no image processing.

    A teammate wrote the first reactive controller on top of it: it aims at the midpoint of the nearest visible blue and yellow cones.

    Repository: TLSe_Racing_Driverlessthe simulator

  2. November and December 2025

    A viewer for the planners

    In PathPlanning, three planners build a closed reference line around a cone track from the full cone map: a midpoint centre line fitted with a B-spline (one teammate), and two RRT* variants with smoothing (another teammate). My part is the Pygame viewer that moves a car along the line, its 2D camera, and the CSV loader for the 26 bundled cone maps.

    The planners see every cone: there is no perception in this repository. By git blame at the end of 2025: 634 lines by the teammate who wrote the RRT* planners, 355 by me, 146 by the teammate who wrote the midpoint planner.

    Repository: PathPlanningoffline planners and viewer

  3. October 2026

    The closed loop

    A controller that never reads the map. Every 20 ms it senses the cones inside the sensor sector, remembers them, pairs blue and yellow cones into gates, chains the gates ahead of the car into a local centre line and steers by pure pursuit with a speed target. A kinematic bicycle model moves the car, and a referee times the laps and counts cone contacts against the true map.

    Written with AI coding assistance: the commits carry a Co-Authored-By trailer. It lives in its own package, separate from the 2025 files.

    Repository: TLSe_Racing_Driverlessclosed_loop/, scripts/, tests/

  4. October 2026

    Measuring the planners

    A command line to run any planner on any of the 26 tracks, measures of the planned lines (closest approach to a cone, share of the line that stays between the two rows), a benchmark of 78 runs with its results committed, tests and CI. The planners themselves were not changed.

    Same AI assistance, same trailer.

    Repository: PathPlanningplanner registry, metrics, benchmark

The closed loop: what the car computes

  1. 01Track CSVthe full cone map: only the sensor and the referee read it
  2. Every 20 ms: sense, remember, pair, order, steer, move. The new pose goes back to the sensor.

    1. 02Field-of-view sensorrange and opening angle (2025)
    2. 03Cone memorymerged within 0.5 m, used after three sightings, forgotten 6 s after the last one
    3. 04Gates and local centre lineblue and yellow pairs 2 m to 6.5 m wide that pass the Gabriel test, chained ahead of the car
    4. 05Pure pursuit and speed targetlookahead 2 m to 5 m, speed limited by lateral acceleration
    5. 06Kinematic bicycle modelsteering and acceleration limits
  3. 07Refereelap timer, cone-hit counter, off-course check, on the true state
My codeInput dataThe sensor model is the 2025 file; the rest of the loop is from October 2026. The simulator gives the car its exact pose, so there is no odometry error, and perception is a visibility test on the true map: there is no camera or LiDAR model. A cone with no partner still extends the line, offset by half a track width towards the inside, which matters with a short sensor range.
The three planners of PathPlanning on small_track, same seed as the benchmark. The car moves at the same constant speed along each line, so the shortest line finishes first. It is an animation of the viewer, not a vehicle model.

Results

4 / 4

bundled tracks driven for three valid laps with the default sensor, 4 m range and 100° field of view. No cone was touched on three of them. A lap is valid when the car went through at least 95% of the track’s reference gates.

25.3→19.5s

Best simulated lap on belgium as the sensor range grows from 4 m to 12 m (and the field of view from 100° to 120°). The speed rule only lets the car go as fast as it can slow down within the line it knows. The car is a kinematic bicycle without tyres: this compares settings of the simulator and does not predict a car.

What was measuredValueHow to read it
Default sensor, 4 m range and 100°: valid laps on belgium, the hairpin track, peanut and small_track3 / 3 on eachCones touched per lap: 0 on belgium, peanut and small_track, 11 on the hairpin track.
Best lap on belgium with a 4 m / 100°, an 8 m / 120° and a 12 m / 120° sensor25.28 s, 20.32 s, 19.54 sLap times fall as the range grows. The range does not change the cone contacts.
No cone memory, 4 m / 100°, on the hairpin trackleft the track at 77 sWithout memory the car also touches cones on two other tracks (3.0 per lap on peanut, 2.0 on small_track).
No one-sided fallback, 4 m / 100°, on the four tracksleft all fourAt 29 s, 28 s, 8 s and 32 s: the fallback is what keeps the line going when the far edge of the track is out of sight.
Detection noise of 0.1 m, then 0.2 m per axis, 4 m / 100°no change, then 2 tracks lostAt 0.2 m the car leaves belgium and the hairpin track. The noise is independent from one cycle to the next, which the running mean of the memory averages away; a biased or drifting error would be harder.
Closed loop, 32 runs of three laps on the four bundled tracks, 3,387 s of simulated driving. The simulation is deterministic; the only random numbers are the detection noise of two ablations, with a fixed seed. The numbers are written by scripts/benchmark.py and committed with the repository. Lap times follow from the limits chosen and from a model without tyres.
Four maps of cone tracks with the path driven in magenta: belgium, peanut, small_track and the long hairpin track. Blue cones on one side, yellow on the other. On the hairpin track a few cones near the last hairpins are circled in white.
Three laps per track with the default sensor, drawn by scripts/render_laps.py. The cones the car touched are circled in white: all of them on the last five hairpins of the hairpin track.
What was measuredValueHow to read it
Median planning time over the 26 tracks: midpoint, rrt, rrt-lsq63 ms, 27.7 s, 34.1 sThe midpoint planner takes 0.2 s at most. The RRT* planners take up to 58.8 s and 73.1 s on one track.
Median closest approach to a cone centre: midpoint, rrt, rrt-lsq0.94 m, 0.15 m, 0.04 mTracks where the line passes closer than 0.7 m to a cone, half the width of a 1.4 m car (an assumption): 6 of 26, 23 of 26 and 25 of 26.
RRT* searches that returned a path585 / 11,998All of them on the three tracks of the original menu (31 of 31, 489 of 491 and 65 of 65). None of 11,411 on the 23 other maps.
small_track, closest approach to a cone: midpoint against rrt-lsq1.92 m → 0.58 mThe RRT* line is shorter (100.1 m against 104.1 m) but passes closer to the cones.
Planners of PathPlanning, 78 runs (three planners on 26 tracks), seed 0, offline planning on the full cone map: no perception, no vehicle model. Planning times were taken while the runs shared the machine, so they are indicative. There is no ground-truth best line: a larger clearance is not a faster lap.

What failed or is unfinished

11

cones touched per lap on the hairpin track, with the default sensor. The laps are completed, but the contacts are all on the last five hairpins.

3 / 26

tracks on which the RRT* planners find a path between waypoints: 585 of 11,998 searches returned one, all on the three tracks of the original menu.

  • The hairpin track is not driven cleanly. There the centre line tightens to a radius of 2.13 m, below the 2.65 m minimum turning radius of the simulated car, whose footprint then sweeps over the cones. Pure pursuit only follows the centre line: avoiding the contacts would need a planner that uses the width of the track, which the repository does not have.

  • The RRT* planners solve only three of the 26 tracks. Every cone is a disc of 1.2 m in the search, so the middle of a gate narrower than 2.4 m lies inside the discs of its own two cones. On the 23 other maps the median gate is 2.20 m to 2.22 m wide. Their lines are then straight segments between the midpoint waypoints, smoothed without any knowledge of the cones: a median closest approach of 0.14 m for rrt and 0.03 m for rrt-lsq on those maps, against 0.94 m for midpoint over all 26 tracks.

  • The first reactive prototype strays. The November 2025 controller aims at the midpoint of the nearest visible blue and yellow cones at a constant speed, keeps no memory and has nothing to do when no cone is in view. Replayed off-screen, the car strays more than 4 m from the middle of the track on all four bundled tracks within 40 simulated seconds. The closed loop adds the memory and the pairing that it lacks.

  • The first sensor setting was too narrow. The 7 m, 60° sector of the sensor model, as I first committed it in November 2025, is enough on three tracks. On the hairpin track the car leaves the track after 52 s: the narrow sector does not show enough of a tight bend.

  • Simulation only. The car knows its exact pose, and perception is a visibility test on the true map: no camera or LiDAR model, no occlusion, no missed or false detection. The cone memory would need a real estimate of the car’s position to work on a vehicle. Camera-based cone detection, localisation and mapping, a tyre model, a racing line and a ROS interface are not in these repositories, and nothing here has run on a car.

Nine small maps, three planners on three tracks. Top row small_track and middle row peanut: a red line running between the blue and yellow cones for midpoint, rrt and rrt-lsq. Bottom row Zandvoort_cones: the line follows the cone rows very closely for all three planners, with 0 of 476 RRT* searches solved.
Midpoint, rrt and rrt-lsq on small_track, peanut and Zandvoort_cones, drawn by scripts/render_planners.py. Bottom row: on a circuit-shaped map no RRT* search returns a path, and the smoothed line then passes a few centimetres from cones.

Credits

  • One teammate wrote the first reactive controller and the offline centre-line prototype of the simulator repository, and the midpoint planner of PathPlanning. Another teammate wrote the two RRT* planners and their smoothing, and the original French note of PathPlanning. The commit history of both repositories shows who wrote what.

  • I wrote the track loader, the camera, the viewer and the field-of-view sensor model in November 2025, then the closed-loop package, the referee, the benchmarks, the figures, the tests and the CI in October 2026.

  • The October 2026 work was written with AI coding assistance, and those commits carry a Co-Authored-By trailer. Every number and every picture on this page is rewritten by a script of the repository.

  • The origin of the four tracks of the simulator repository and of the 26 maps of PathPlanning is not documented in the repositories.

  • Beyond these repositories, I also worked on cone-detection models in PyTorch and on image-processing and control modules with ROS, Python and C++. That work is not in them, and this page shows none of it.

  • Pure pursuit follows R. C. Coulter (1992), the pairing test is the Gabriel graph (Gabriel and Sokal, 1969), RRT* is Karaman and Frazzoli (2011). The GIFs are recorded with ffmpeg.