# The Problem
Formula 1 moves hundreds of tons of cargo between 24 Grands Prix across 4 continents every season. The order of the calendar determines how many kilometres that cargo travels, how much it costs to move and how much CO2e it emits. This project reorders the 2025 calendar to minimize cost and emissions while respecting real sporting, commercial and climate constraints.
Formally it is a TSP with time windows: an NP-hard problem where finding the shortest route is not enough, because each venue only admits certain dates and some rules cannot be broken. With 24 circuits the number of possible orderings is astronomical, yet the problem still falls within what an exact solver can handle.
The challenge: minimize cost and carbon footprint without violating a single constraint — and be able to prove it, not just claim it.
The approach: solve it with two independent methods and compare both against the official calendar using exactly the same model and the same audit.
# Results
Three calendars evaluated with the same logistics model and the same constraint verifier. The official 2025 calendar is the baseline:
| Calendar | Method | Distance | Cost | CO2e | Violations |
|---|---|---|---|---|---|
| Official 2025 | baseline | 121,376 km | $43.86M | 78,693 t | 0 |
| OR-Tools | CP-SAT (exact optimum) | 93,161 km | $34.69M | 61,006 t | 0 |
| Genetic | Metaheuristic + 2-opt | 93,161 km | $34.69M | 61,006 t | 0 |
The savings do not come from cutting races but from reordering them: the optimum eliminates the 8 most expensive transfers of the official calendar and replaces them with short regional legs. 16 of the 24 Grands Prix change round. No figure is hand-written: they are all regenerated by running the pipeline.
# Why two algorithms
Solving the problem twice, with methods of a different nature, is the methodological core of the project — not redundancy:
| OR-Tools CP-SAT | Genetic algorithm | |
|---|---|---|
| Type | Exact solver (constraint programming) | Evolutionary metaheuristic |
| Guarantee | Certified optimum | Good solution, no theoretical guarantee |
| Time | < 1 second | ~ seconds |
| Scales to | Problems expressible as linear/logical constraints | Any cost function, even non-linear or black-box |
- CP-SAT gives the business answer: the certified exact optimum. With 24 circuits the problem is still tractable for an exact solver, and giving up that guarantee would be unjustifiable.
- The genetic algorithm validates its own implementation against that optimum: it reached exactly the same solution, with a 0.00% gap. Without a known optimum you never know whether a metaheuristic delivers a good result or merely a plausible one. Here you do.
- The genetic algorithm is the approach that survives when the problem grows: if the model incorporated non-linear objectives, uncertainty or many more venues, the exact solver would stop scaling and the metaheuristic — already validated — would be the production tool.
# Constraints & audit
The model's seven constraints are hard: there are no negotiable penalties and no margin for non-compliance. Every run audits all three solutions and reports any violation.
First race: Melbourne
F1 tradition
Last race: Abu Dhabi
F1 tradition
Races on Sunday
Real GP dates
Minimum logistics interval
7 days (<2,500 km) or 14 days on long legs, with waiting weeks allowed
Climate window per circuit
Each GP within its viable date range
No two consecutive races in the same country
Commercial constraint
Season from Mar 1 to Dec 15
2025 season limits
A result is only reported as valid if it complies 100%. This audit is what separates a real saving from an apparent one: it is easy to make a calendar cheaper if you ignore that the cargo has to arrive on time and within the right climate window.
# Multimodal logistics model
Each leg is computed according to its transport mode: air freight (~600 t) on intercontinental jumps, road (~1,300 t in trucks) within Europe, and 5 sets of sea kits (~2,400 t) as a fixed annual overhead.
| Mode | Cost (official → optimum) | CO2e | Distance | Emission factor |
|---|---|---|---|---|
| Air | $37.73M → $29.04M | 73,737 → 56,766 t | 114,320 → 88,009 km | ~0.60 kgCO2e/t·km |
| Road | $1.81M → $1.32M | 2,652 → 1,936 t | 7,056 → 5,152 km | ~0.105 kgCO2e/t·km |
| Sea (kits) | $4.32M (fixed) | 2,304 t | 60,000 km | ~0.016 kgCO2e/t·km |
Emission factors of DEFRA 2024 order and IATA/market 2024 rates, with the source cited parameter by parameter in the repository. Scope note: this is an estimation model to compare calendars against each other, not a logistics quotation. The comparison is valid because all three calendars are evaluated with exactly the same model.
# Visualization
The pipeline generates its own reports. Both open in a new tab and are the actual files the project produces, untouched:
Executive dashboard
Comparison of the three calendars: master table with deltas, breakdown by transport mode, the most expensive transfers and which ones the optimum removes, round changes per GP, leg-by-leg detail, constraint audit and the genetic algorithm's convergence towards the optimum.
Open dashboard 🌍Interactive 3D globe
WebGL globe with animated arcs for each calendar, an Official / OR-Tools / Genetic selector with its KPIs, and tooltips per race and per leg. Free rotation and zoom.
Open globeThe 3D globe loads its library and textures from a CDN, so it needs an internet connection to render.
# Inside the solver
The heart of the project is 55 lines: the complete CP-SAT model. A Hamiltonian circuit with boolean arcs, dates restricted to the set of valid Sundays for each venue, temporal propagation per arc, and consecutive countries forbidden by construction. The objective minimizes logistics cost in whole dollars.
# src/f1logistics/ortools_optimizer.py
class OrToolsOptimizer:
def _sundays_in_window(self, i):
"""Domingos (días de temporada) dentro de la ventana climática del circuito i."""
first = self.model._next_sunday(int(self.dm.win_start[i]))
return list(range(first, int(self.dm.win_end[i]) + 1, 7))
def run(self):
"""Devuelve (route, days, status_name). Lanza RuntimeError si es infactible."""
dm, lm = self.dm, self.model
n = dm.n
m = cp_model.CpModel()
# Fechas: solo domingos dentro de la ventana de cada circuito
day = []
for i in range(n):
sundays = self._sundays_in_window(i)
if not sundays:
raise RuntimeError(f"El circuito {dm.df.iloc[i]['Ciudad']} no tiene "
"ningún domingo dentro de su ventana climática")
day.append(m.NewIntVarFromDomain(cp_model.Domain.FromValues(sundays), f"day_{i}"))
# Arcos del circuito hamiltoniano
arcs, lit = [], {}
for i in range(n):
for j in range(n):
if i == j:
continue
if i == dm.end_idx and j == dm.start_idx:
# Arco virtual de cierre: obligatorio, sin costo ni secuencia temporal
closing = m.NewBoolVar("closing")
m.Add(closing == 1)
arcs.append((i, j, closing))
continue
if j == dm.start_idx or i == dm.end_idx:
continue # nadie más entra al inicio ni sale del final
if dm.countries[i] == dm.countries[j]:
continue # restricción dura: sin países consecutivos
x = m.NewBoolVar(f"x_{i}_{j}")
lit[(i, j)] = x
arcs.append((i, j, x))
m.Add(day[j] >= day[i] + int(lm.leg_gap[i, j])).OnlyEnforceIf(x)
m.AddCircuit(arcs)
# Objetivo: costo logístico entero en USD
m.Minimize(sum(int(round(lm.leg_cost[i, j])) * x for (i, j), x in lit.items()))
solver = cp_model.CpSolver()
solver.parameters.max_time_in_seconds = float(self.time_limit_s)
solver.parameters.num_workers = int(self.workers)
status = solver.Solve(m)
if status not in (cp_model.OPTIMAL, cp_model.FEASIBLE):
raise RuntimeError(f"CP-SAT no encontró solución factible")
# Reconstruir la ruta siguiendo los arcos activos desde Melbourne
nxt = {i: j for (i, j), x in lit.items() if solver.Value(x)}
route = [dm.start_idx]
while route[-1] != dm.end_idx:
route.append(nxt[route[-1]])
days = [solver.Value(day[i]) for i in route]
return route, days, solver.StatusName(status) # src/f1logistics/ortools_optimizer.py
class OrToolsOptimizer:
def _sundays_in_window(self, i):
"""Domingos (días de temporada) dentro de la ventana climática del circuito i."""
first = self.model._next_sunday(int(self.dm.win_start[i]))
return list(range(first, int(self.dm.win_end[i]) + 1, 7))
def run(self):
"""Devuelve (route, days, status_name). Lanza RuntimeError si es infactible."""
dm, lm = self.dm, self.model
n = dm.n
m = cp_model.CpModel()
# Fechas: solo domingos dentro de la ventana de cada circuito
day = []
for i in range(n):
sundays = self._sundays_in_window(i)
if not sundays:
raise RuntimeError(f"El circuito {dm.df.iloc[i]['Ciudad']} no tiene "
"ningún domingo dentro de su ventana climática")
day.append(m.NewIntVarFromDomain(cp_model.Domain.FromValues(sundays), f"day_{i}"))
# Arcos del circuito hamiltoniano
arcs, lit = [], {}
for i in range(n):
for j in range(n):
if i == j:
continue
if i == dm.end_idx and j == dm.start_idx:
# Arco virtual de cierre: obligatorio, sin costo ni secuencia temporal
closing = m.NewBoolVar("closing")
m.Add(closing == 1)
arcs.append((i, j, closing))
continue
if j == dm.start_idx or i == dm.end_idx:
continue # nadie más entra al inicio ni sale del final
if dm.countries[i] == dm.countries[j]:
continue # restricción dura: sin países consecutivos
x = m.NewBoolVar(f"x_{i}_{j}")
lit[(i, j)] = x
arcs.append((i, j, x))
m.Add(day[j] >= day[i] + int(lm.leg_gap[i, j])).OnlyEnforceIf(x)
m.AddCircuit(arcs)
# Objetivo: costo logístico entero en USD
m.Minimize(sum(int(round(lm.leg_cost[i, j])) * x for (i, j), x in lit.items()))
solver = cp_model.CpSolver()
solver.parameters.max_time_in_seconds = float(self.time_limit_s)
solver.parameters.num_workers = int(self.workers)
status = solver.Solve(m)
if status not in (cp_model.OPTIMAL, cp_model.FEASIBLE):
raise RuntimeError(f"CP-SAT no encontró solución factible")
# Reconstruir la ruta siguiendo los arcos activos desde Melbourne
nxt = {i: j for (i, j), x in lit.items() if solver.Value(x)}
route = [dm.start_idx]
while route[-1] != dm.end_idx:
route.append(nxt[route[-1]])
days = [solver.Value(day[i]) for i in route]
return route, days, solver.StatusName(status) Code exactly as it is in the repository — verifiable line by line.
- data/ — circuits with coordinates and climate windows, parameters with cited sources and championship constraints.
- model.py — multimodal model, date scheduling and constraint auditor.
- ortools_optimizer.py — exact CP-SAT solver.
- ga_optimizer.py — genetic algorithm: tournament selection with elitism, OX crossover, swap/inversion mutation and a final 2-opt local search.
- reports.py — comparative dashboard and 3D globe.
- tests/ — 19 tests, including specific regressions for the v1 errors.
Want to audit the full repo?
View on GitHub# Lessons from version 1
The first version of this project reported a 26.5% reduction in distance. That number was false. Its calendar violated 8 of the 24 climate windows without reporting it — the date model did not allow waiting, which made it impossible to reach Abu Dhabi in November — the air emission factor was inflated by roughly 1000×, and constraints were not audited after optimizing.
This version fixes the model, audits everything and accepts a smaller (-20.9% cost) but defensible saving. The tests include specific regressions for each of those errors so they cannot come back. I publish this because a result that cannot be audited is worth nothing, and because finding your own mistakes is part of the job — not a footnote.