Skip to content
spade-codeePublic

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

2 Commits

Folders and files

Repository files navigation

Race Intelligence

Motorsport simulation, strategy and telemetry platform.

A quasi-steady-state lap-time solver drives a tyre degradation model, an exact strategy optimiser and a vectorised Monte Carlo race engine. Every number the interface displays is traceable to the model that produced it.

DATA  →  MODEL  →  SIMULATION  →  STRATEGY  →  VISUALISATION

Race Intelligence dashboard

Race dashboard — circuit map coloured by solved speed, live timing tower, tyre strategy timeline and race pace, all from one seeded simulation.


What this is

An open engineering demonstration of the modelling chain behind race strategy: how circuit geometry becomes corner speeds, how corner speeds become lap times, how lap times plus a tyre model become a strategy, and how a strategy plus uncertainty becomes a probability distribution.

What this is not

  • It does not reproduce any Formula 1 team's internal simulator.
  • It has no access to proprietary telemetry or tyre data.
  • The drivers and constructors are fictional.
  • Circuit geometry is an approximation authored from public layout information, not a survey.

Every API response carries a provenance label — PUBLIC, MODEL_OUTPUT, SIMULATED or ESTIMATED — and the interface displays it. See Data honesty.


Quick start

Two processes. No database to provision, no API keys, no external services.

Backend

cd backend
python -m venv .venv
.venv/Scripts/activate          # Windows
# source .venv/bin/activate     # macOS / Linux
pip install -r requirements-dev.txt
uvicorn app.main:app --port 8000

The API is then on http://127.0.0.1:8000, with interactive docs at /docs. On startup it integrates all five circuits, solves their reference laps, and warms the per-driver pace cache on a background thread (about one second total).

Frontend

cd frontend
npm install
npm run dev

Open http://localhost:3000. Next.js proxies /api/* to the backend, so the browser makes same-origin requests and there is no CORS to configure.

Tests

cd backend
.venv/Scripts/python -m pytest        # 164 tests

Model validation

cd backend
python -m scripts.calibrate           # residuals against published lap records
python -m scripts.calibrate --fit     # refit the global grip calibration

The workflow

The platform is built around one continuous analysis path:

  1. Select a circuit — geometry is integrated from a curvature spine.
  2. Explore the map — eight visualisation modes over one solved lap.
  3. Compare two drivers — synchronised telemetry with a shared cursor.
  4. Inspect tyre degradation — with every term of the model broken out.
  5. Optimise a strategy — exact dynamic programming over every legal plan.
  6. Run 10,000 races — vectorised Monte Carlo, about two seconds.
  7. Stress-test it — re-optimise and re-simulate under perturbations.
  8. Replay a race — lap by lap, with the field animating around the circuit.
  9. Ask the race engineer — answers cite the computed values they rest on.

Telemetry — one cursor, every channel

Telemetry comparison

Speed, throttle, brake, gear, RPM and lateral load all derive from one solved speed profile, so the channels are mutually consistent by construction rather than drawn independently. Move the cursor anywhere — on a trace or on the map — and every panel follows: distance, corner name, sector, and the delta at that exact point.

Strategy — exact optimisation, then stress-tested

Strategy optimiser

Race time decomposes so strategies compare exactly on tyre cost plus pit loss. Optimal stint splits are solved by dynamic programming — the true optimum of the deterministic model, not the best of a sampled set. The what-if panel then re-optimises and re-simulates under perturbed assumptions, because a plan that is 0.4 s faster but collapses under a safety car is not the better plan.

Race engineer — constrained to computed values

Race view with the AI race engineer

Ask it whether to pit and it answers from an exhaustive evaluation of every remaining stop lap, quoting the degradation rate, the pit cost and both branch totals. Every figure it reports appears in the citations list and in the inspectable context below the answer, because it is never asked to calculate — only to phrase what the models already computed.

Replay — the whole race as one picture

Race replay with position tape

The position tape under the map is every driver's race position across every lap. A strategy that worked shows up as a line climbing through the field.

Circuit intelligence

Circuit explorer


Architecture

race-intelligence/
├── backend/
│   ├── app/
│   │   ├── models/          physical models (the engineering core)
│   │   │   ├── parameters.py    every constant, in one place
│   │   │   ├── track.py         curvature spine → integrated geometry
│   │   │   ├── vehicle.py       point-mass forces, friction ellipse
│   │   │   ├── laptime.py       quasi-steady-state solver
│   │   │   └── tyre.py          degradation model
│   │   ├── simulation/      race engines
│   │   │   ├── race_model.py    shared pace model
│   │   │   ├── race_engine.py   single race, event-recording
│   │   │   ├── monte_carlo.py   vectorised ensemble
│   │   │   └── jobs.py          background job registry
│   │   ├── strategy/        exact optimiser + what-if
│   │   ├── telemetry/       synthetic channel generation
│   │   ├── engineer/        AI race engineer
│   │   ├── data/            circuits, entrants, provider abstraction
│   │   ├── schemas/         Pydantic contracts
│   │   ├── db/              SQLAlchemy schema
│   │   └── api/routes/      FastAPI endpoints
│   ├── scripts/calibrate.py
│   └── tests/
├── frontend/
│   ├── app/                 Next.js App Router pages
│   ├── components/
│   │   ├── circuit/         interactive track map
│   │   ├── charts/          hand-built SVG chart primitives
│   │   ├── race/            timing tower, strategy timeline
│   │   ├── engineer/        race engineer panel
│   │   └── ui/              interface primitives
│   ├── lib/                 typed API client, formatting, hooks
│   └── store/               cross-view session state
└── docs/
    ├── architecture.md
    ├── simulation-model.md
    ├── tyre-model.md
    ├── strategy-engine.md
    └── data-sources.md

Full detail in docs/architecture.md.


Technology

Backend — Python 3.12+, FastAPI, Pydantic v2, NumPy, SQLAlchemy.

Frontend — Next.js 14 (App Router), React 18, TypeScript, Tailwind CSS, Zustand.

Two deliberate omissions:

  • No charting library. The telemetry view needs one cursor synchronised across six stacked traces and a track map, which fights most libraries' event models. The charts are hand-built SVG, which also keeps first-load JS at ~120 kB.
  • No task queue. Simulation is NumPy, which releases the GIL, so a daemon thread genuinely runs in parallel with the event loop. The whole platform is one process with nothing to deploy alongside it. JobRegistry is narrow enough that swapping in Celery later means implementing four methods.

Methodology

Lap time

The classic three-pass quasi-steady-state algorithm. Circuits are authored as curvature spines — sequences of straights and constant-radius arcs — and integrated up to position, because the lap-time model needs curvature and differentiating a hand-drawn polyline twice produces noise that swamps the signal.

kappa(s)  →  corner speed limit  →  backward pass (braking)
                                 →  forward pass (traction)
                                 →  v(s) = min of the three
                                 →  t = ∫ ds/v

Two corrections take the model from textbook to credible: a superelliptic friction envelope (exponent 1.7 rather than a friction circle) and a transient load-build window that stops the car snapping between lateral limits instantaneously. The second is what makes a direction-change circuit lose more than a power circuit — an effect that emerges rather than being asserted.

Validation

Modelled qualifying laps against published lap records, with one global grip calibration for the whole library — never a per-circuit correction, which would hide a bad geometry behind a good-looking table.

Circuit Modelled Published record Error
Silverstone 1:25.266 1:27.097 −2.10%
Monza 1:23.310 1:20.329 +3.71%
Spa 1:43.033 1:46.286 −3.06%
Suzuka 1:28.879 1:30.983 −2.31%
Monaco 1:13.576 1:12.909 +0.91%

RMS error 2.60%, mean bias −0.57%, grip calibration 1.000.

A qualifying lap should sit a few percent inside a race lap record, so the negative residuals are the expected direction. Monza's positive residual is the one genuine outlier: that record dates from 2004, under a different technical regulation. The report is served live at GET /api/circuits/validation and shown on the landing page.

Strategy

Total race time decomposes exactly:

T = Σ_laps [ base_pace + fuel_effect·fuel(lap) − evolution(lap) ]
  + Σ_stints Σ_age tyre_delta(compound, age)
  + n_stops · pit_loss

The first sum is identical for every strategy and cancels out of any comparison. The tyre term depends only on compound and age within the stint, never on which lap it falls. So the strategy problem separates, and the optimal stint split is solved exactly by dynamic programming — a min-plus convolution over stint lengths — rather than by sampling candidate stop laps.

Stochastic effects break the decomposition, which is exactly why the Monte Carlo engine exists. The workflow is: rank exactly, then re-rank under uncertainty.

Monte Carlo

Ten thousand races as array operations over a (n_simulations, n_drivers) state matrix, looping only over laps. About 50 iterations of 200k-element NumPy work instead of 10⁷ Python iterations.

~5,800 races/second. Deterministic for a fixed seed.


Data honesty

Label Meaning
PUBLIC Published reference material — lap distances, corner counts, lap records, safety car base rates
MODEL_OUTPUT Computed by the deterministic physical models — corner speeds, lap times, strategy costs
SIMULATED Produced by the stochastic simulation — race results, telemetry, probabilities
ESTIMATED Hand-authored parameters that were not measured — tyre coefficients, driver coefficients, geometry

A data provider that cannot serve a request raises DataNotAvailable rather than inventing an answer. PublicDataProvider is deliberately unimplemented and raises on every method: a silent fallback to synthetic data is precisely how fabricated data ends up presented as real.

The AI Race Engineer is constrained the same way. Every quantity it reports is computed first and assembled into a structured context; the language model — when one is configured — is given those facts and asked to phrase them, never to calculate. Without an API key it runs deterministically from templates. Both modes return citations listing the model outputs the answer rests on, and the full computed context is inspectable in the UI.


Limitations

Stated plainly, because a model whose limits are hidden is not an engineering artefact.

Vehicle model — point mass. No weight transfer, suspension kinematics, tyre slip curves, differential behaviour, transient aerodynamics, yaw dynamics, brake temperature, engine maps or ERS state of charge.

Lap-time model — no driver model and no racing line. It drives the geometric centreline, which is longer through corners than a real line.

Geometry — approximate. Lap distance and corner count match published figures; radii and arc angles do not. Elevation is authored per segment and only weakly coupled to the vehicle model.

Tyre model — coefficients are hand-authored estimates. The temperature penalty is symmetric, which is the model's weakest simplification: a real tyre degrades far faster when overheating than when cold.

Race model — a car can gain at most one position per lap. Damage, penalties, driver error and weather transitions are not modelled.

Monte Carlo — pit stops are drawn up front, so the ensemble cannot model a driver reacting to a safety car by pitting opportunistically. Safety car timing is modelled and its effect on gaps applied; only the reactive stop is missing. This is the largest known bias in the ensemble.

Known fidelity gap — Monza should be the highest full-throttle circuit in the library (~78% in reality) but the model puts it at ~68%, behind Spa. The authored chicane radii are tighter than the real ones relative to their entry speeds. Recorded rather than tuned away.


Extending it

Replace the synthetic data. Implement DataProvider (see app/data/provider.py), map the source's identifiers onto the platform's, resample telemetry onto the uniform distance grid, set provenance = "PUBLIC", and register it in get_provider(). Nothing else changes.

Use PostgreSQL. The schema in app/db/models.py is written for Postgres and runs on SQLite unchanged. Point RI_DATABASE_URL at a Postgres instance and install psycopg[binary].

Enable the assisted race engineer. Set RI_ANTHROPIC_API_KEY. The engineer still computes every number itself; the model only phrases them.

Bring a MATLAB/Simulink model. app/models/parameters.py is the hand-off point. A model identified offline exports its fitted coefficients straight into VehicleParameters or TyreParameters without touching simulation code. See docs/architecture.md.


API

GET    /api/health
GET    /api/model-info                          every constant the models use
GET    /api/providers

GET    /api/circuits
GET    /api/circuits/validation                 model error vs lap records
GET    /api/circuits/{id}
GET    /api/circuits/{id}/elevation
GET    /api/circuits/{id}/lap                   solve under custom conditions

GET    /api/drivers
GET    /api/drivers/teams
GET    /api/drivers/{id}
GET    /api/drivers/{id}/analytics

GET    /api/telemetry/{circuit}/{driver}/{lap}
GET    /api/telemetry/{circuit}/compare

POST   /api/simulations/race                    synchronous, ~60 ms
POST   /api/simulations/monte-carlo             async job
GET    /api/simulations/jobs/{id}
GET    /api/simulations/jobs/{id}/result
DELETE /api/simulations/jobs/{id}

POST   /api/strategies/analyze
POST   /api/strategies/evaluate
POST   /api/strategies/what-if
POST   /api/strategies/tyre-projection

GET    /api/engineer/status
POST   /api/engineer/ask

Every error returns the same shape, so the UI has exactly one error renderer:

{ "detail": { "code": "...", "message": "...", "resource": "...", "hint": "..." } }

Deploying to a VPS

Three containers behind Caddy, which obtains and renews a Let's Encrypt certificate on its own — no certbot, no cron job.

# 1. Point an A record for your domain at the VPS
# 2. On the VPS:
git clone <your-repo> race-intelligence && cd race-intelligence
cp .env.example .env && nano .env          # set DOMAIN and ACME_EMAIL
docker compose up -d --build

That is the whole deployment. Caddy serves the app on /, proxies /api/* to the backend on the same origin (so there is no CORS anywhere in production), and exposes the OpenAPI docs at /docs.

Neither application container publishes a port to the host; only Caddy is reachable from outside. Both run unprivileged, and the backend is CPU-capped so a 50,000-race request cannot starve the web tier on a small box.

Resources — comfortable on 2 vCPU / 2 GB. The backend holds all five circuits' solved geometry in memory (a few MB) and a 10,000-race ensemble peaks around 200 MB.

One worker, deliberately. Simulation jobs live in an in-process registry, so a second uvicorn worker would serve job ids it cannot see. Scaling out means moving JobRegistry to Redis first.

Backups — the only state is backend-data (SQLite) and caddy-data (certificates). Losing the latter just means re-issuing.


Documentation

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages