A lap time simulator written in MATLAB. Give it a circuit and a car, and it searches for the racing line and the speed profile that minimise the lap time — then renders the result at 1:1 scale.
Personal project, built from scratch. Still in development — I know there is more to add, and what comes next is written down rather than left vague.
F1 2026 preset at Silverstone — 88.3 s, 6.1 g peak. The car is 1.90 m wide on an 11-to-18 m ribbon, and it is never scaled up to read better on screen.
| Racing line | Minimum-curvature QP, constrained by the track width |
| Speed | Forward-Backward integration over a speed-dependent acceleration envelope |
| Vehicle | Point mass, or a four-wheel model (loads, μ(T, Fz), transfer, brake balance) |
| Circuits | A generated Formula Student autocross; Monza and Silverstone from the TUM database |
| Output | Lap time, analysis figures, and a 1:1 scale 3D render |
The physics simulator is the only judge. Every candidate — including the ones proposed by a learned model — is scored by the same Forward-Backward pass. Nothing in this project grades its own output.
| Skidpad | Autocross | Monza | Silverstone | |
|---|---|---|---|---|
| Length | 57.3 m | 234.0 m | 5 790.5 m | 5 887.2 m |
| Centreline (naive) | 4.825 s | 15.905 s | 143.576 s | — |
| Optimised line | 4.539 s | 13.475 s | 133.795 s | — |
| Gain | −5.9 % | −15.3 % | −6.8 % | — |
| Solve time | 1.2 s | 0.9 s | 6.4 s | — |
Monza measures 5 790.5 m against 5 793 m official — 0.04 % error on the TUM geometry. The gain is smaller there than on the autocross, which is exactly right: on a straight there is no line to optimise.
| Autocross | Monza | |
|---|---|---|
| Baseline setup | 13.455 s | 123.229 s |
| Optimised setup | 12.135 s | 116.407 s |
| Gain | −1.320 s | −6.822 s |
Cl found |
−2.38 (at the bound) | −0.84 (interior optimum) |
The downforce/drag trade reverses between circuits. On Monza it is interior: even on the fastest track of the set, the car wants some downforce for the chicanes — but only just.
| Point mass | Four wheels | |
|---|---|---|
| Autocross, FS car | 13.475 s | 13.709 s (+0.234) |
| Monza, F1 2026 preset | — | 81.038 s · 326 km/h |
| Silverstone, F1 2026 preset | — | 88.341 s · 324 km/h · 6.08 g |
The four-wheel model is slower, and that is the expected result: load sensitivity makes weight transfer a net loss, the tyres are not at their optimum temperature, and traction is limited by the rear axle alone. The point mass replaced all three with a single coefficient.
![]() |
![]() |
| F1 2026 preset — 5.50 × 1.90 m, bodywork built from lofted superelliptic cross-sections. | Formula Student car — 2.80 × 1.20 m, roll hoops, high rear wing on visible struts. |
The four tyre temperatures around a lap of Silverstone. The rear left runs hottest — it is the outside wheel through the fast right-handers that dominate this circuit.
Full videos (13 clips, 2560 × 1440): see the Releases tab. The repository keeps only GIFs so it stays quick to clone.
circuit + car
│
├─ Method A ────── minimum-curvature QP, blend-weight sweep
│ every candidate scored by a real Forward-Backward lap
│
├─ vehicle model
│ ├─ point mass → friction ellipse, single μ
│ └─ four wheels → 4 vertical loads, μ(T, Fz), weight transfer,
│ brake balance, driven rear axle
│ └─ envelope_to_gg → 3 curves
│
└─ Forward-Backward ── speed profile, THE SAME in both cases
The judge never changes. Method A does not return an "optimal" line in the QP sense. It produces a family of candidates and makes them all drive, then keeps the fastest. The criterion is lap time, not integrated curvature.
The four-wheel model does not touch the solver. It resolves the four contact patches off-line
and hands over three curves — ay_max(v), traction, braking — in the shape gg_diagram already
consumed. A vehicle without that field takes the old path instruction for instruction, which
is what keeps every earlier lap record bit-exact.
Temperature closes a loop from the outside. Grip depends on tyre temperature, tyre temperature depends on how hard the lap is driven, and Forward-Backward has no state to hold a temperature in. So: envelope → lap → temperatures along that lap → envelope again. Damped by half, without which it cycles instead of settling.
These four cost real time, and each one forced a change deeper than a patch.
A comparison that rewarded the wrong winner. In the elimination race, each line was compared
against the others on its own arc length. A longer trajectory therefore looked like it was
ahead. Fixed by projecting everyone onto the circuit's arc length
(PaceReference = "track"), and the ranking inverted.
A numerical guard mistaken for physics. v_max = 45 m/s was a solver guard rail, not a
property of the car. On Monza it clipped the lap, and 12.2 of the seconds credited to an
aerodynamic gain were nothing but that clip. It is now lifted per run, never globally.
A penalty measured on the wrong quantity. Off-track distance was computed on the clipped line rather than the one actually simulated. The surrogate model was therefore learning noise: 0.2 s validation error against a 3 s expected gain.
A slip curve inconsistent with its own definition. The supplied formula returned 0.30 at the operating slip of a tyre whose peak is documented as 1. Applied as written it made the F1 car slower than the Formula Student one. I normalised it and wrote down why, instead of copying it and shipping a wrong number.
Stated here rather than discovered by a careful reader.
- No DRS. The JSON fields exist; nothing reads them.
- No suspension. Only the hook is written; vertical loads stay quasi-static.
- Aerodynamics are four numbers (
Cl,Cd,S, balance). No map, no ride height, no dirty air, no wing flex. - Tyres are three effects, not a Pacejka fit — nobody publishes the coefficients of a current racing tyre.
- The F1 lap times are orders of magnitude, not predictions. TUM centreline, one aero package for every circuit. The OpenF1 cross-check gives +23 km/h at Monza and +7 at Silverstone: reported, not corrected — recalibrating on one circuit would falsify the other.
- The learning loop does not beat Method A. Gains are hundredths of a second. That is in the report rather than hidden.
- The renderer is MATLAB graphics, not a game engine.
cd FS_MinLapTime
addpath scripts
setup_project % paths, vehicle parameters, project status
run_optimization % optimise, plot, save the recordA few useful entry points:
run_full_pipeline("monza") % line, energy, figures, animation
run_swarm_visualization("autocross", N = 30) % the elimination race
run_v5_setup_opt("monza") % search Cl, Cd, mu
run_v10_lap("silverstone", "f1_2026") % a lap with the four-wheel model
run_v11_cinematic("monza", "f1_2026") % the render
build_linkedin_pack % the videos and their captionsThe car is a JSON file you load, paint and edit:
car = load_car("f1_2026");
car.aero.Cl = -2.60;
save_car(car, "cars/monza_pack.json");
run_v10_lap("monza", "cars/monza_pack.json");Required: MATLAB R2026a and the Optimization Toolbox. Everything else is optional — the project detects what is installed and falls back to an in-repo implementation otherwise.
From a Linux shell driving a MATLAB session on Windows, both working on one tree with no
copying: the repository lives in WSL 2, MATLAB opens it over \\wsl.localhost\, and git,
gh and ffmpeg stay native on the Linux side.
Ubuntu 24.04 (WSL 2) Windows 11
/root/FS_MinLapTime MATLAB R2026a
git · gh · ffmpeg · bash cd \\wsl.localhost\Ubuntu\...
└──────── the same files ────────┘
It costs a handful of specific gotchas — save -v7.3 fails over the SMB layer, MATLAB cannot
receive change notifications so rehash path is mandatory after creating a file, and a
long-lived session hides a broken one until you start cold. All of them are written down, with
what they cost, in docs/workflow.md.
| Folder | |
|---|---|
optim/ |
Method A, Method B, Forward-Backward |
vehicle/, data/ |
vehicle and tyre models, acceleration envelope |
cars/ |
JSON cars, loader, editor, OpenF1 cross-check |
tracks/ |
track generation and TUM import |
swarm/ |
the elimination race |
setup_opt/, ml/, ml_driver/ |
setup search, learning loops |
unreal/ |
track render and cinematic render |
scripts/ |
the entry points |
docs/ |
detailed documentation |
Source code, function headers and comments are in English throughout, as are the ten documents
linked below. Eight further notes in docs/ — the version-by-version development log and the
per-module write-ups nothing here links to — are still in French, and say so at the top. Ask and I
will translate any of them.
docs/architecture.md |
data flow, what each file is for |
docs/optimization_method.md |
Methods A, B, B_smooth |
docs/vehicle_model.md |
point mass and the g-g diagram |
docs/vehicle_v10.md |
JSON cars, four-wheel tyres, editor |
docs/track_format.md |
track format and TUM import |
docs/swarm_visualization.md |
the elimination race |
docs/setup_optimization.md |
setup search, drag polar |
docs/ml_training.md |
learning loop, line encoding |
docs/cinematic_render.md |
the renderer, lofts, camera |
docs/how_to_run.md |
step-by-step tutorial |
docs/workflow.md |
the development setup: a Linux shell driving MATLAB |
docs/development_log.md |
the full log, V1 → V12 |
This model is still under development, and I am well aware there is more to add. What runs today is listed above. What I have already identified, and have not written yet:
- Suspension.
vehicle/suspension_none.malready fixes the signature a real model must respect. What it will change is the split of lateral load transfer between the axles — which is what roll stiffness distribution actually controls, and the single biggest simplification left in the vehicle model. - Per-corner tyre temperature instead of a lap average. The per-point trace is real; the feedback onto grip is currently averaged over the lap.
- Coupled ax/ay solved across the four contact patches, rather than the friction ellipse used today.
- Wear and compounds — this simulates one lap, not a stint.
- Validation against real track data, which is the one thing that would turn orders of magnitude into predictions.
Each version so far started from a written specification and ended with a report that says what
did not work as well as what did — the full history is in
docs/development_log.md. I would rather show a model whose limits I
can name than one that looks finished.
Code under the MIT licence — see LICENSE.
The Monza and Silverstone geometries come from
TUMFTM/racetrack-database (LGPL v3) and are not
redistributed here; download_tum_tracks(["Monza", "Silverstone"], Import = true) fetches them.
Cross-check speeds come from OpenF1, read-only.
The f1_2026 preset is calibrated on the public FIA 2026 regulations and published orders of
magnitude. It uses no team data and no licensed tyre model, and its source field states which
of its numbers are regulatory and which are estimates.









