Earlier work · Age 15

Formula 1 Race Simulator

A lap-by-lap simulation of a 2022 Formula 1 race in Python, driven by real telemetry rather than invented numbers. Built as my A-Level computer science project, and the first time I worked out that the interesting part of a simulation is where the parameters come from.

A note on this one: I wrote it at fifteen, as school coursework, years before anything else on this site. The code is not representative of how I write now and I would make different choices throughout. It is here for the reasoning behind it rather than the implementation.

Deriving car performance from telemetry

The obvious way to build a race simulator is to give every car a single speed rating and let the dice do the rest. That produces a random number generator wearing a Formula 1 costume, and it cannot explain why a car that dominates in Monaco struggles at Monza.

Instead I pulled qualifying and race telemetry through the FastF1 library and decomposed each car into four numbers: its pace through high, medium and low speed corners, and its top speed. A circuit is then described by how many corners of each type it has and how much straight it contains. Car performance at a given track falls out of matching one against the other, so the same car is genuinely fast at some circuits and slow at others for reasons traceable to its measured behaviour.

Extracting those numbers had a subtlety I got wrong first time. My initial approach read each driver’s speed at the same index in their telemetry array, which quietly assumes every driver set an identical lap time. They do not, so a slower driver’s lap contains more samples and the same index lands somewhere else on the circuit entirely. I fixed it by switching from absolute indices to proportional position: take one driver as reference, establish that a given corner occurs at some percentage through their lap, and read every other driver at that same percentage through theirs.

Driver form is harder, because unlike a car it moves during a season. I scraped per-driver season ratings from a motorsport news site, which updates them race by race.

The twenty-minute load time

Originally every performance value was computed from FastF1 at runtime. Constructing a race meant twenty cars, each calculating corner and straight performance separately, plus the circuit: over forty library calls, all requiring an internet connection. Startup took more than twenty minutes, which did not just make the program unpleasant. It made it untestable, because no one iterates on a bug they can only reproduce twice an hour.

So I moved the extraction offline and built databases holding the derived values, leaving the simulation to read from them. Load time collapsed and the program became entirely self-contained.

I have since done a version of this on nearly every project on this site: separate the expensive derivation of parameters from the cheap running of the model, so the part you iterate on stays fast. I did not have that principle at fifteen. I had a program I could not test, which is a more memorable way to learn it.

A note on this one: I wrote it at fifteen, as school coursework, several years before anything else on this site. The code is not representative of how I write now, and I would make different choices throughout. It is here because the reasoning behind it still holds up, not the implementation.

Race model and interface

Each lap resolves overtaking attempts, applies tyre wear by compound, evaluates pit stop decisions and compound choice, and determines retirements. Position changes are conditioned on race context rather than drawn blind: a driver who pitted last lap cannot immediately attack, and a driver whose rival has just pitted gets a much better chance of gaining the place. Weather is set per event and feeds the same machinery.

The interface is a top-down tkinter view updating once per lap, structured on a model, view and controller split. It also produced my first encounter with an event loop: the simulation ran correctly but the window stayed frozen until the race finished, since the race loop never returned control to the GUI. Forcing an update inside the loop fixed it.

The project was tested against a written test plan and reviewed with a user, whose verdict was that results were mostly plausible but that the slower teams finished too high and there were too many retirements. Both are fair, and both point at the same thing: the model resolves each lap sensibly but nothing constrains the race-level distribution to look like a real one.

  • Python
  • FastF1
  • Web scraping
  • SQL
  • tkinter
  • MVC