Formula 1 Scheduling and Routing Optimization#
Description: A notebook that tackles the Formula 1 Calendar as a routing and a scheduling problem, minimizing total distance between races whilst also assigning a spot in the calendar respecting scheduling constraints using MP
Tags: sports, gurobi, mp, scheduling, f1
Notebook author: Eduardo Salazar <eduardo@ampl.com>
Model author: Eduardo Salazar <eduardo@ampl.com>
# Install dependencies
%pip install -q amplpy pandas matplotlib seaborn folium
# Google Colab & Kaggle integration
from amplpy import AMPL, ampl_notebook
ampl = ampl_notebook(
modules=["gurobi"], # modules to install
license_uuid="default", # license to use
) # instantiate AMPL object and register magics
The Challenge#
Formula 1 faces a complex scheduling puzzle every season: arranging 24 races across the globe while minimizing massive logistical costs. The 2026 season includes races from Australia to Abu Dhabi, creating a travel network that spans over 100,000 kilometers if poorly planned.
Currently, F1 calendar planning relies heavily on manual scheduling that considers broadcast timing, venue availability, and regional clustering. However, this approach often results in inefficient routing - teams might race in Asia, then fly to the Americas, then return to Europe, accumulating unnecessary travel distance and costs.
Why This Matters#
The implications extend beyond simple logistics:
Economic Impact: F1 teams spend millions annually on freight and personnel travel
Environmental Concerns: Inefficient routing increases carbon emissions from cargo flights
Operational Complexity: Poor scheduling strains team resources and affects performance
Strategic Planning: Teams need predictable schedules for resource allocation
The Mathematical Approach#
This notebook demonstrates how mathematical programming can optimize complex real-world scheduling problems. Using AMPL’s logical constraint capabilities, we model the F1 calendar as an optimization problem with multiple competing objectives and constraints:
Objective: Minimize total travel distance between consecutive race venues
Hard Constraints: Fixed season start/end points, mandatory summer break, venue availability
Operational Rules: Limits on consecutive race weekends, spacing requirements between race clusters
What You’ll See#
We’ll compare an optimized calendar against the actual 2026 F1 schedule, revealing potential improvements in routing efficiency. The analysis includes:
Mathematical model formulation using AMPL’s MP logical constraints
Interactive visualizations showing global race routing
Quantified comparison of travel distances and scheduling patterns
Calendar views highlighting the optimization’s constraint satisfaction
This case study illustrates how operations research techniques can improve real-world logistics, even in highly constrained environments like international motorsport scheduling.
# Import additional libraries
from datetime import datetime, timedelta
from math import atan2, cos, radians, sin, sqrt, degrees
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import folium
from folium import plugins
import seaborn as sns
from IPython.display import display, HTML
print("F1 Calendar Optimization: MP Logical Constraints Demonstration")
print("=" * 70)
print("This notebook showcases AMPL's Mathematical Programming (MP) capabilities")
print("using logical constraints (<==> syntax) for complex scheduling optimization.")
print("\nWe'll compare our optimized solution with the actual 2026 F1 calendar!")
F1 Calendar Optimization: MP Logical Constraints Demonstration
======================================================================
This notebook showcases AMPL's Mathematical Programming (MP) capabilities
using logical constraints (<==> syntax) for complex scheduling optimization.
We'll compare our optimized solution with the actual 2026 F1 calendar!
# ========================================
# 1. F1 CIRCUIT COORDINATES & REAL 2026 CALENDAR
# ========================================
CIRCUIT_COORDINATES = {
"Australia": (-37.8497, 144.9680), # Albert Park, Melbourne
"China": (31.3389, 121.2197), # Shanghai International Circuit
"Japan": (34.8431, 136.5411), # Suzuka Circuit
"Bahrain": (26.0325, 50.5106), # Bahrain International Circuit
"Saudi Arabia": (21.6319, 39.1044), # Jeddah Corniche Circuit
"Miami": (25.9581, -80.2389), # Miami International Autodrome
"Canada": (45.5000, -73.5228), # Circuit Gilles Villeneuve, Montreal
"Monaco": (43.7347, 7.4206), # Circuit de Monaco
"Spain Barcelona": (41.5700, 2.2611), # Circuit de Barcelona-Catalunya
"Austria": (47.2197, 14.7647), # Red Bull Ring, Spielberg
"Great Britain": (52.0786, -1.0169), # Silverstone Circuit
"Belgium": (50.4372, 5.9714), # Circuit de Spa-Francorchamps
"Hungary": (47.5789, 19.2486), # Hungaroring, Budapest
"Netherlands": (52.3888, 4.5409), # Circuit Zandvoort
"Italy": (45.6156, 9.2811), # Autodromo Nazionale Monza
"Spain Madrid": (40.4168, -3.7038), # Madrid (IFEMA-Madring circuit)
"Azerbaijan": (40.3725, 49.8533), # Baku City Circuit
"Singapore": (1.2914, 103.8644), # Marina Bay Street Circuit
"Austin": (30.1328, -97.6411), # Circuit of the Americas, Austin
"Mexico": (19.4042, -99.0907), # Autódromo Hermanos Rodríguez
"Brazil": (-23.7036, -46.6997), # Interlagos, São Paulo
"Las Vegas": (36.1147, -115.1728), # Las Vegas Strip Circuit
"Qatar": (25.4900, 51.4542), # Lusail International Circuit
"Abu Dhabi": (24.4672, 54.6031), # Yas Marina Circuit
}
# Real 2026 F1 Calendar (Official)
REAL_2026_CALENDAR = [
("Australia", "2026-03-08"),
("China", "2026-03-15"),
("Japan", "2026-03-29"),
("Bahrain", "2026-04-12"),
("Saudi Arabia", "2026-04-19"),
("Miami", "2026-05-03"),
("Canada", "2026-05-24"),
("Monaco", "2026-06-07"),
("Spain Barcelona", "2026-06-14"),
("Austria", "2026-06-28"),
("Great Britain", "2026-07-05"),
("Belgium", "2026-07-19"),
("Hungary", "2026-07-26"),
("Netherlands", "2026-08-23"),
("Italy", "2026-09-06"),
("Spain Madrid", "2026-09-13"), # New track!
("Azerbaijan", "2026-09-26"), # Saturday race
("Singapore", "2026-10-11"),
("Austin", "2026-10-25"),
("Mexico", "2026-11-01"),
("Brazil", "2026-11-08"),
("Las Vegas", "2026-11-21"), # Saturday night
("Qatar", "2026-11-29"),
("Abu Dhabi", "2026-12-06"),
]
print(f"{len(CIRCUIT_COORDINATES)} F1 circuits in optimization")
print(f"Real 2026 calendar: {len(REAL_2026_CALENDAR)} races")
24 F1 circuits in optimization
Real 2026 calendar: 24 races
# ========================================
# 2. DISTANCE CALCULATION FUNCTIONS
# ========================================
def calculate_geodesic_distance(coord1, coord2):
"""Calculate great circle distance using Haversine formula"""
lat1, lon1 = map(radians, coord1)
lat2, lon2 = map(radians, coord2)
dlat = lat2 - lat1
dlon = lon2 - lon1
a = sin(dlat / 2) ** 2 + cos(lat1) * cos(lat2) * sin(dlon / 2) ** 2
c = 2 * atan2(sqrt(a), sqrt(1 - a))
R = 6371 # Earth's radius in km
return R * c
def create_distance_matrix():
"""Create geodesic distance matrix between all circuits"""
circuits = list(CIRCUIT_COORDINATES.keys())
n = len(circuits)
distance_matrix = np.zeros((n, n))
print("Computing geodesic distances...")
for i, circuit1 in enumerate(circuits):
for j, circuit2 in enumerate(circuits):
if i != j:
distance = calculate_geodesic_distance(
CIRCUIT_COORDINATES[circuit1], CIRCUIT_COORDINATES[circuit2]
)
distance_matrix[i][j] = distance
return pd.DataFrame(distance_matrix, index=circuits, columns=circuits)
def calculate_total_distance(calendar_sequence, distance_matrix):
"""Calculate total distance for a given race sequence"""
total = 0
for i in range(len(calendar_sequence) - 1):
race1 = calendar_sequence[i]
race2 = calendar_sequence[i + 1]
total += distance_matrix.loc[race1, race2]
return total
Mathematical Programming Model: F1 Calendar Optimization#
Mathematical Programming Overview#
Mathematical Programming (MP) represents optimization problems using mathematical relationships between decision variables, objectives, and constraints. Unlike simple heuristics or manual planning, MP guarantees finding optimal solutions within defined constraint boundaries.
This F1 scheduling problem exemplifies Mixed-Integer Programming with Logical Constraints - a sophisticated class of optimization that combines:
Binary variables (yes/no decisions)
Integer variables (discrete choices like week numbers)
Logical relationships (if-then-else rules) through the use of AMPL MP.
Our Model Architecture#
Decision Variables#
var x{RACES, POSITIONS} binary; # Assignment: race r in position p
var w{POSITIONS} integer >= 10, <= 49; # Week number for each position
var consecutive{p in 1..23} binary; # Consecutive week indicator
var triple_start{p in 1..22} binary; # Triple header start indicator
Objective Function#
We minimize total great-circle distance between consecutive race venues:
minimize TotalDistance:
sum{p in 1..23} sum{r1 in RACES, r2 in RACES}
distance[r1, r2] * x[r1, p] * x[r2, p+1];
This formulation automatically selects the optimal race sequence by penalizing long-distance transitions.
Advanced Logical Constraints#
The model’s sophistication lies in its logical constraint system using AMPL’s MP <==> (if-and-only-if) syntax:
Consecutive Race Definition#
subject to DefineConsecutive{p in 1..23}:
consecutive[p] <==> (w[p+1] = w[p] + 1);
This creates a bidirectional logical relationship: consecutive[p] is true if and only if races p and p+1 occur in consecutive weeks.
Triple Header Management#
subject to DefineTripleStart{p in 1..22}:
triple_start[p] <==> (consecutive[p] = 1 and consecutive[p+1] = 1);
Defines triple headers (three consecutive race weekends) as a compound logical condition.
Complex Operational Rules#
subject to NoMoreThan3InARow{p in 1..21}:
(consecutive[p] = 1 and consecutive[p+1] = 1) ==> consecutive[p+2] = 0;
subject to BreakAfterTriple{p in 1..21}:
triple_start[p] = 1 ==> w[p+3] >= w[p+2] + 2;
These constraints encode real F1 operational requirements: no more than three consecutive race weekends, with mandatory breaks after intense periods.
Why This Showcases MP Power#
This problem demonstrates several key MP capabilities:
Complex Constraint Interactions: Summer break requirements, consecutive race limits, and distance optimization create competing objectives that simple heuristics cannot balance effectively.
Logical Constraint Modeling: The <==> syntax allows natural expression of “if-and-only-if” relationships that would require complex reformulations in basic linear programming.
Real-World Validation: Comparing against the actual 2026 F1 calendar provides concrete evidence of optimization benefits.
Scalability: The model structure could easily extend to additional constraints (weather patterns, broadcast preferences, venue costs) without fundamental reformulation.
Model Complexity#
The complete formulation includes:
600+ binary variables (24 races × 25 positions + consecutive indicators)
24 integer variables (week assignments)
50+ constraints (logical, operational, and assignment rules)
Nonlinear relationships (a quadratic objective)
Technical Innovation#
The model demonstrates several advanced MP techniques:
Logical constraints for complex rule modeling
Geodesic distance calculations for realistic objective functions
Temporal constraint networks for scheduling optimization
This combination showcases MP’s ability to handle real-world complexity while maintaining mathematical rigor and solution optimality.
The following cells implement this mathematical model using AMPL and demonstrate its application to the 2026 F1 calendar optimization.
%%writefile f1.mod
# ========================================
# F1 CALENDAR OPTIMIZATION MODEL
# ========================================
set RACES;
set POSITIONS = 1..24;
param distance{RACES, RACES} >= 0;
# ========================================
# VARIABLES
# ========================================
# Assignment variables
var x{RACES, POSITIONS} binary;
# Week assignment
var w{POSITIONS} integer >= 10, <= 49;
# Binary variable: 1 if race p+1 is in the week immediately after race p
var consecutive{p in 1..23} binary;
# Binary variable: 1 if this starts a triple header (3 consecutive races)
var triple_start{p in 1..22} binary;
# ========================================
# OBJECTIVE
# ========================================
minimize TotalDistance:
sum{p in 1..23} sum{r1 in RACES, r2 in RACES}
distance[r1, r2] * x[r1, p] * x[r2, p+1];
# ========================================
# CORE CONSTRAINTS
# ========================================
# Assignment constraints
subject to OnePositionPerRace{r in RACES}:
sum{p in POSITIONS} x[r, p] = 1;
subject to OneRacePerPosition{p in POSITIONS}:
sum{r in RACES} x[r, p] = 1;
# Fixed endpoints
subject to StartAustralia:
x['Australia', 1] = 1;
subject to EndAbuDhabi:
x['Abu Dhabi', 24] = 1;
# Week ordering (at least 1 week between races)
subject to StrictWeekOrder{p in 1..23}:
w[p] + 1 <= w[p+1];
# Start and end weeks
subject to FirstWeek:
w[1] = 10;
subject to LastWeek:
w[24] = 49;
# ========================================
# SUMMER BREAK CONSTRAINTS
# ========================================
# No races in weeks 31-33
subject to SummerBreak{p in POSITIONS}:
w[p] <= 30 or w[p] >= 34;
# Ensure break happens around middle
subject to BeforeBreak:
w[12] <= 30;
subject to AfterBreak:
w[13] >= 34;
# ========================================
# DEFINE CONSECUTIVE RACES
# ========================================
# Define when races are in consecutive weeks
subject to DefineConsecutive{p in 1..23}:
consecutive[p] <==> (w[p+1] = w[p] + 1);
subject to NonConsecutiveGap{p in 1..23}:
(not consecutive[p]) ==> (w[p+1] >= w[p] + 2);
# Define triple header starts
# A triple header starts at position p if p, p+1, p+2 are all consecutive
subject to DefineTripleStart{p in 1..22}:
triple_start[p] <==> (consecutive[p] = 1 and consecutive[p+1] = 1);
# ========================================
# MAIN CONSECUTIVE RACE CONSTRAINTS
# ========================================
# 1. NO MORE THAN 3 RACES IN A ROW - ABSOLUTE MAXIMUM
# This means if we have 3 consecutive races, the 4th CANNOT be consecutive
subject to NoMoreThan3InARow{p in 1..21}:
(consecutive[p] = 1 and consecutive[p+1] = 1) ==> consecutive[p+2] = 0;
# Alternative formulation: If three races are consecutive, break the chain
subject to BreakAfter3Consecutive{p in 1..21}:
consecutive[p] + consecutive[p+1] + consecutive[p+2] <= 2;
# 2. MAXIMUM 2 TRIPLE HEADERS IN THE ENTIRE CALENDAR
subject to MaxTwoTripleHeaders:
sum{p in 1..22} triple_start[p] <= 2;
# 3. AFTER A DOUBLE HEADER (exactly 2 consecutive), NEED A BREAK
# If p and p+1 are consecutive but p+2 is not consecutive, then need gap after p+1
subject to BreakAfterDouble{p in 1..22}:
(consecutive[p] = 1 and consecutive[p+1] = 0) ==> w[p+2] >= w[p+1] + 2;
# 4. AFTER A TRIPLE HEADER (exactly 3 consecutive), NEED A BREAK
# If p starts a triple header, then race p+3 must have a gap
subject to BreakAfterTriple{p in 1..21}:
triple_start[p] = 1 ==> w[p+3] >= w[p+2] + 2;
# ========================================
# ADDITIONAL SAFETY CONSTRAINTS
# ========================================
# Explicit prevention of 4+ consecutive races
subject to Prevent4Consecutive{p in 1..21}:
not (w[p+1] = w[p] + 1 and w[p+2] = w[p+1] + 1 and w[p+3] = w[p+2] + 1);
# Ensure gaps between clusters
subject to GapBetweenClusters{p in 2..22}:
(consecutive[p-1] = 1 and consecutive[p] = 0 and consecutive[p+1] = 1) ==>
w[p+1] >= w[p] + 2;
Writing f1.mod
# ========================================
# 4. SOLUTION PROCESSING
# ========================================
def solve_mp_model(distance_matrix):
"""Solve the MP model and extract solution"""
print("\n" + "=" * 50)
print("SOLVING WITH AMPL MP LOGICAL CONSTRAINTS")
print("=" * 50)
# Use the global AMPL instance created in setup
global ampl
ampl.reset() # Clear any previous model
ampl.read("f1.mod")
races = list(distance_matrix.index)
ampl.set["RACES"] = races
ampl.param["distance"] = distance_matrix
# Configure Gurobi (already installed via ampl_notebook)
ampl.option["solver"] = "gurobi"
ampl.option["gurobi_options"] = "timelimit=600 outlev=1"
print("Solving with MP logical constraints...")
print(" Model features:")
print(f" • {len(races)} races, {24} positions")
print(f" • {len(races)*24 + 24 + 23 + 22} decision variables")
print(" • Logical constraints with <==> operators")
print(" • Complex clustering and timing constraints")
ampl.solve()
solve_result = ampl.get_value("solve_result")
print(f"Solver status: {solve_result}")
if "infeasible" in solve_result.lower():
print("Model is infeasible!")
return None, None
# Extract solution
x_vals = ampl.var["x"]
w_vals = ampl.var["w"]
optimized_calendar = []
optimized_weeks = []
for p in range(1, 25):
for race in races:
if x_vals[race, p].value() > 0.5:
week = int(w_vals[p].value())
optimized_calendar.append(race)
optimized_weeks.append(week)
break
total_distance = ampl.get_objective("TotalDistance").value()
print(f"Optimized total distance: {total_distance:,.0f} km")
return optimized_calendar, optimized_weeks, total_distance
# ========================================
# 5. VISUALIZATION FUNCTIONS - PART 1: MAP HELPERS
# ========================================
def calculate_great_circle_points(coord1, coord2, num_points=100):
"""
Calculate intermediate points along a great circle path.
Properly handles crossing the International Date Line.
"""
lat1, lon1 = map(radians, coord1)
lat2, lon2 = map(radians, coord2)
# Calculate the great circle distance
dlon = lon2 - lon1
# Adjust for crossing the date line (shortest path)
if abs(dlon) > np.pi:
if dlon > 0:
dlon = -(2 * np.pi - dlon)
else:
dlon = 2 * np.pi + dlon
points = []
for i in range(num_points + 1):
fraction = i / num_points
# Calculate intermediate point using spherical interpolation
a = sin((1 - fraction) * abs(dlon)) / sin(abs(dlon))
b = sin(fraction * abs(dlon)) / sin(abs(dlon))
# Handle the special case where points are very close
if abs(dlon) < 0.001:
# Linear interpolation for very close points
lat = lat1 * (1 - fraction) + lat2 * fraction
lon = lon1 * (1 - fraction) + lon2 * fraction
else:
# Spherical interpolation
x = a * cos(lat1) * cos(lon1) + b * cos(lat2) * cos(
lon2 if dlon >= 0 else lon2 - 2 * np.pi
)
y = a * cos(lat1) * sin(lon1) + b * cos(lat2) * sin(
lon2 if dlon >= 0 else lon2 - 2 * np.pi
)
z = a * sin(lat1) + b * sin(lat2)
lat = atan2(z, sqrt(x**2 + y**2))
lon = atan2(y, x)
points.append([degrees(lat), degrees(lon)])
return points
def create_improved_folium_map(calendar, circuit_coords, title, color="blue"):
"""
Create an improved Folium map that handles trans-oceanic routes correctly.
"""
# Create base map
m = folium.Map(location=[20, 0], zoom_start=2, max_bounds=True)
# Add all circuit markers
for i, race in enumerate(calendar):
lat, lon = circuit_coords[race]
# Add circuit marker
folium.CircleMarker(
location=[lat, lon],
radius=8,
popup=f"<b>{i+1}. {race}</b>",
tooltip=f"{i+1}. {race}",
color=color,
fill=True,
fillColor=color,
fillOpacity=0.7,
weight=2,
).add_to(m)
# Add race number label
folium.Marker(
location=[lat, lon],
icon=folium.DivIcon(
html=f"""<div style="
font-size: 12px;
color: white;
font-weight: bold;
background-color: {color};
border-radius: 50%;
width: 20px;
height: 20px;
text-align: center;
line-height: 20px;
">{i+1}</div>""",
icon_size=(20, 20),
icon_anchor=(10, 10),
),
).add_to(m)
# Add route lines with proper great circle handling
for i in range(len(calendar) - 1):
race1 = calendar[i]
race2 = calendar[i + 1]
coord1 = circuit_coords[race1]
coord2 = circuit_coords[race2]
# Check if route crosses the International Date Line
lon1, lon2 = coord1[1], coord2[1]
# If longitude difference is > 180, it's a trans-Pacific route
if abs(lon2 - lon1) > 180:
# Split the route at the date line
if lon1 > 0 and lon2 < 0: # Westward crossing (e.g., Asia to Americas)
# Create two segments
mid_lat = (coord1[0] + coord2[0]) / 2
# Segment 1: From origin to date line (180°)
folium.PolyLine(
[[coord1[0], coord1[1]], [mid_lat, 180]],
color=color,
weight=2,
opacity=0.6,
smooth_factor=1,
).add_to(m)
# Segment 2: From date line (-180°) to destination
folium.PolyLine(
[[mid_lat, -180], [coord2[0], coord2[1]]],
color=color,
weight=2,
opacity=0.6,
smooth_factor=1,
).add_to(m)
elif lon1 < 0 and lon2 > 0: # Eastward crossing (e.g., Americas to Asia)
# Create two segments
mid_lat = (coord1[0] + coord2[0]) / 2
# Segment 1: From origin to date line (-180°)
folium.PolyLine(
[[coord1[0], coord1[1]], [mid_lat, -180]],
color=color,
weight=2,
opacity=0.6,
smooth_factor=1,
).add_to(m)
# Segment 2: From date line (180°) to destination
folium.PolyLine(
[[mid_lat, 180], [coord2[0], coord2[1]]],
color=color,
weight=2,
opacity=0.6,
smooth_factor=1,
).add_to(m)
else:
# Normal route that doesn't cross date line
# Use great circle points for curved path
points = calculate_great_circle_points(coord1, coord2, num_points=50)
folium.PolyLine(
points, color=color, weight=2, opacity=0.6, smooth_factor=1
).add_to(m)
# Add title
title_html = f"""
<h3 align="center" style="font-size:18px; margin: 10px;"><b>{title}</b></h3>
"""
m.get_root().html.add_child(folium.Element(title_html))
return m
# ========================================
# 5. VISUALIZATION FUNCTIONS - PART 2: MAP CREATION
# ========================================
def create_static_world_map(calendar, circuit_coords, title, color="green", ax=None):
"""
Create a static matplotlib visualization with proper great circle routes.
This avoids all projection issues by using matplotlib's built-in capabilities.
"""
if ax is None:
fig = plt.figure(figsize=(20, 10))
ax = plt.subplot(
111, projection="mollweide"
) # Mollweide projection shows the whole world nicely
# Convert coordinates for Mollweide projection (requires radians)
def transform_coords(lat, lon):
# Mollweide expects longitude in [-pi, pi] and latitude in [-pi/2, pi/2]
return radians(lon), radians(lat)
# Draw base map features
ax.grid(True, alpha=0.3)
ax.set_facecolor("#e6f2ff") # Light blue for oceans
# Plot all circuits
for i, race in enumerate(calendar):
lat, lon = circuit_coords[race]
x, y = transform_coords(lat, lon)
# Plot circuit point
ax.plot(
x,
y,
"o",
color=color,
markersize=10,
markeredgecolor="white",
markeredgewidth=1,
zorder=5,
)
# Add text label (offset slightly to avoid overlap)
ax.text(
x,
y + 0.05,
str(i + 1),
fontsize=8,
ha="center",
va="bottom",
color="darkblue",
fontweight="bold",
zorder=6,
)
# Draw routes with great circle paths
for i in range(len(calendar) - 1):
race1 = calendar[i]
race2 = calendar[i + 1]
lat1, lon1 = circuit_coords[race1]
lat2, lon2 = circuit_coords[race2]
# Check if we need to handle date line crossing
if abs(lon2 - lon1) > 180:
# Trans-Pacific or trans-Atlantic route
if lon1 > 0 and lon2 < 0: # Westward
# Adjust longitude for shortest path
lon2_adjusted = lon2 + 360
elif lon1 < 0 and lon2 > 0: # Eastward
# Adjust longitude for shortest path
lon1_adjusted = lon1 + 360
lon1, lon2 = lon2, lon1_adjusted
lat1, lat2 = lat2, lat1
# Create interpolated points for smooth curve
num_points = 100
lons = np.linspace(
lon1,
(
lon2
if abs(lon2 - lon1) <= 180
else lon2 + 360 if lon2 < lon1 else lon2 - 360
),
num_points,
)
# Normalize longitudes to [-180, 180]
lons = ((lons + 180) % 360) - 180
# Simple latitude interpolation (good enough for visualization)
lats = np.linspace(lat1, lat2, num_points)
# Transform and plot
xs, ys = [], []
for lat, lon in zip(lats, lons):
x, y = transform_coords(lat, lon)
xs.append(x)
ys.append(y)
# Handle discontinuity at date line
# Split the path where there's a large jump in x coordinates
segments = []
current_segment_x = [xs[0]]
current_segment_y = [ys[0]]
for j in range(1, len(xs)):
if (
abs(xs[j] - xs[j - 1]) > np.pi
): # Large jump indicates date line crossing
# End current segment
segments.append((current_segment_x, current_segment_y))
# Start new segment
current_segment_x = [xs[j]]
current_segment_y = [ys[j]]
else:
current_segment_x.append(xs[j])
current_segment_y.append(ys[j])
# Add final segment
segments.append((current_segment_x, current_segment_y))
# Plot all segments
for seg_x, seg_y in segments:
ax.plot(
seg_x, seg_y, "-", color=color, linewidth=1.5, alpha=0.6, zorder=3
)
else:
# Normal route
x1, y1 = transform_coords(lat1, lon1)
x2, y2 = transform_coords(lat2, lon2)
# Create curved path
num_points = 50
lons = np.linspace(lon1, lon2, num_points)
lats = np.linspace(lat1, lat2, num_points)
xs = [radians(lon) for lon in lons]
ys = [radians(lat) for lat in lats]
ax.plot(xs, ys, "-", color=color, linewidth=1.5, alpha=0.6, zorder=3)
# Labels and title
ax.set_title(title, fontsize=16, fontweight="bold", pad=20)
ax.set_xlabel("Longitude", fontsize=12)
ax.set_ylabel("Latitude", fontsize=12)
# Create custom x-axis labels
ax.set_xticks([-np.pi, -np.pi / 2, 0, np.pi / 2, np.pi])
ax.set_xticklabels(["180°W", "90°W", "0°", "90°E", "180°E"])
ax.set_yticks([-np.pi / 2, -np.pi / 4, 0, np.pi / 4, np.pi / 2])
ax.set_yticklabels(["90°S", "45°S", "0°", "45°N", "90°N"])
return ax
def create_dual_map_comparison(
optimized_calendar, real_calendar, circuit_coords, optimized_distance, real_distance
):
"""
Create side-by-side static maps for better comparison.
This completely avoids the trans-Pacific routing issues.
"""
fig, (ax1, ax2) = plt.subplots(
1, 2, figsize=(24, 10), subplot_kw={"projection": "mollweide"}
)
# Optimized route
create_static_world_map(
optimized_calendar,
circuit_coords,
f"MP Optimized Route\nTotal: {optimized_distance:,.0f} km",
color="green",
ax=ax1,
)
# Real 2026 route
real_races = [race for race, date in real_calendar]
create_static_world_map(
real_races,
circuit_coords,
f"Real 2026 F1 Calendar\nTotal: {real_distance:,.0f} km",
color="red",
ax=ax2,
)
# Overall title
fig.suptitle(
"F1 Calendar Route Comparison - 2026 Season",
fontsize=18,
fontweight="bold",
y=1.02,
)
# Add savings text
savings = real_distance - optimized_distance
savings_pct = (savings / real_distance) * 100
fig.text(
0.5,
0.02,
f"Optimization Savings: {savings:,.0f} km ({savings_pct:.1f}%)",
ha="center",
fontsize=14,
fontweight="bold",
color="darkgreen",
)
plt.tight_layout()
return fig
# ========================================
# 5. VISUALIZATION FUNCTIONS - PART 3: CALENDAR VIEWS
# ========================================
def create_calendar_view_fixed(
optimized_calendar,
optimized_weeks,
real_calendar,
optimized_distance,
real_distance,
):
"""Create a calendar-style visualization showing weekly schedule using ACTUAL optimized weeks"""
print(f"\nWEEKLY CALENDAR COMPARISON")
print("=" * 80)
# Real 2026 calendar weeks (extracted from dates)
real_weeks = []
for race, date_str in real_calendar:
date_obj = datetime.strptime(date_str, "%Y-%m-%d")
week_num = date_obj.isocalendar()[1]
real_weeks.append(week_num)
print(f"OPTIMIZED SCHEDULE (Total: {optimized_distance:,.0f} km)")
print("-" * 80)
# Create comparison table using ACTUAL optimized weeks
print("Week | Optimized Route | Real 2026 Calendar | Notes")
print("-" * 80)
all_weeks = set(optimized_weeks + real_weeks)
for week in sorted(all_weeks):
if 31 <= week <= 33:
print(
f"{week:2d} | {'SUMMER BREAK':^20} | {'SUMMER BREAK':^20} | 🏖️ Mandatory break"
)
continue
opt_race = ""
real_race = ""
# Find optimized race for this week
if week in optimized_weeks:
idx = optimized_weeks.index(week)
opt_race = optimized_calendar[idx]
# Find real race for this week
if week in real_weeks:
idx = real_weeks.index(week)
real_race = real_calendar[idx][0]
# Notes
note = ""
if opt_race and real_race:
if opt_race == real_race:
note = "Same"
else:
note = "Different"
elif opt_race:
note = "Opt only"
elif real_race:
note = "Real only"
print(f"{week:2d} | {opt_race:^20} | {real_race:^20} | {note}")
print("-" * 80)
print(f"TOTAL DISTANCES:")
print(f" Optimized: {optimized_distance:>10,.0f} km")
print(f" Real 2026: {real_distance:>10,.0f} km")
print(
f" Savings: {real_distance - optimized_distance:>10,.0f} km ({((real_distance - optimized_distance)/real_distance*100):>5.1f}%)"
)
return optimized_weeks, real_weeks
def create_gantt_chart_fixed(
optimized_calendar, real_calendar, optimized_weeks, real_weeks
):
"""Create a Gantt chart visualization using ACTUAL optimized weeks"""
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(16, 10))
# Optimized schedule - use ACTUAL optimized weeks
ax1.barh(
range(len(optimized_calendar)),
[1] * len(optimized_calendar),
left=optimized_weeks,
color="green",
alpha=0.7,
height=0.6,
)
ax1.set_yticks(range(len(optimized_calendar)))
ax1.set_yticklabels(
[f"{i+1}. {race}" for i, race in enumerate(optimized_calendar)], fontsize=8
)
ax1.set_xlabel("Calendar Week")
ax1.set_title(
"MP Optimized F1 Calendar (Actual Optimized Weeks)",
fontsize=14,
fontweight="bold",
)
ax1.set_xlim(8, 52)
ax1.grid(axis="x", alpha=0.3)
# Add summer break highlight
ax1.axvspan(31, 33, alpha=0.3, color="red", label="Summer Break")
ax1.legend()
# Real 2026 schedule
real_races = [race for race, date in real_calendar]
ax2.barh(
range(len(real_races)),
[1] * len(real_races),
left=real_weeks,
color="red",
alpha=0.7,
height=0.6,
)
ax2.set_yticks(range(len(real_races)))
ax2.set_yticklabels(
[f"{i+1}. {race}" for i, race in enumerate(real_races)], fontsize=8
)
ax2.set_xlabel("Calendar Week")
ax2.set_title("Real 2026 F1 Calendar", fontsize=14, fontweight="bold")
ax2.set_xlim(8, 52)
ax2.grid(axis="x", alpha=0.3)
# Add summer break highlight
ax2.axvspan(31, 33, alpha=0.3, color="red", label="Summer Break")
ax2.legend()
plt.tight_layout()
plt.show()
return fig
def analyze_calendars_fixed(
optimized_calendar, optimized_weeks, real_calendar, distance_matrix
):
"""Detailed analysis using optimized weeks"""
print(f"\nDETAILED CALENDAR ANALYSIS WITH ACTUAL WEEKS")
print("=" * 70)
# Real 2026 calendar weeks
real_weeks = []
for race, date_str in real_calendar:
date_obj = datetime.strptime(date_str, "%Y-%m-%d")
week_num = date_obj.isocalendar()[1]
real_weeks.append(week_num)
# Create comparison DataFrame
comparison_data = []
for i in range(24):
opt_race = optimized_calendar[i]
opt_week = optimized_weeks[i]
real_race = real_calendar[i][0]
real_week = real_weeks[i]
# Calculate distance from previous race
if i > 0:
opt_prev_dist = distance_matrix.loc[optimized_calendar[i - 1], opt_race]
real_prev_dist = distance_matrix.loc[real_calendar[i - 1][0], real_race]
else:
opt_prev_dist = 0
real_prev_dist = 0
comparison_data.append(
{
"Position": i + 1,
"Optimized_Race": opt_race,
"Optimized_Week": opt_week,
"Real_Race": real_race,
"Real_Week": real_week,
"Opt_Distance": opt_prev_dist,
"Real_Distance": real_prev_dist,
"Distance_Difference": real_prev_dist - opt_prev_dist,
"Week_Difference": real_week - opt_week,
}
)
df = pd.DataFrame(comparison_data)
print("Pos | Optimized (Week) | Real 2026 (Week) | Week Δ | Dist Δ")
print("-" * 70)
for _, row in df.iterrows():
if row["Position"] == 1:
print(
f"{row['Position']:2d} | {row['Optimized_Race']:<12} (W{row['Optimized_Week']:2d}) | {row['Real_Race']:<12} (W{row['Real_Week']:2d}) | -- | Start"
)
else:
week_diff = row["Week_Difference"]
dist_diff = row["Distance_Difference"]
week_symbol = "=" if week_diff == 0 else f"{week_diff:+d}"
dist_symbol = "↑" if dist_diff > 0 else "↓" if dist_diff < 0 else "="
print(
f"{row['Position']:2d} | {row['Optimized_Race']:<12} (W{row['Optimized_Week']:2d}) | {row['Real_Race']:<12} (W{row['Real_Week']:2d}) | {week_symbol:>5} | {dist_symbol} {abs(dist_diff):>5.0f}"
)
print(f"\nWEEK ANALYSIS:")
print(
f" Optimized calendar spans weeks {min(optimized_weeks)} to {max(optimized_weeks)}"
)
print(f" Real calendar spans weeks {min(real_weeks)} to {max(real_weeks)}")
# Check summer break compliance
summer_break_optimized = [w for w in optimized_weeks if 31 <= w <= 33]
summer_break_real = [w for w in real_weeks if 31 <= w <= 33]
print(f" Summer break (weeks 31-33):")
print(
f" Optimized: {'Respected' if len(summer_break_optimized) == 0 else '❌ Violated'}"
)
print(
f" Real 2026: {'Respected' if len(summer_break_real) == 0 else '❌ Violated'}"
)
# Analyze consecutive races
consecutive_optimized = []
consecutive_real = []
for i in range(len(optimized_weeks) - 1):
if optimized_weeks[i + 1] == optimized_weeks[i] + 1:
consecutive_optimized.append(i + 1)
for i in range(len(real_weeks) - 1):
if real_weeks[i + 1] == real_weeks[i] + 1:
consecutive_real.append(i + 1)
print(f" Consecutive week transitions:")
print(f" Optimized: {len(consecutive_optimized)} consecutive transitions")
print(f" Real 2026: {len(consecutive_real)} consecutive transitions")
return df
# ========================================
# 6. MAIN COMPARISON AND VISUALIZATION
# ========================================
def create_comparison_visualization_fixed(
optimized_calendar,
optimized_weeks,
optimized_distance,
real_calendar,
real_distance,
distance_matrix,
):
"""Create comprehensive comparison using ACTUAL optimized weeks"""
print("\n" + "=" * 60)
print("OPTIMIZATION vs REAL 2026 CALENDAR COMPARISON")
print("=" * 60)
# Calculate savings
distance_savings = real_distance - optimized_distance
percent_savings = (distance_savings / real_distance) * 100
print(f"OPTIMIZED SOLUTION (MP Model):")
print(f" Total distance: {optimized_distance:,.0f} km")
print(f"\nREAL 2026 F1 CALENDAR:")
print(f" Total distance: {real_distance:,.0f} km")
print(f"\nOPTIMIZATION BENEFIT:")
print(f" Distance saved: {distance_savings:,.0f} km ({percent_savings:.1f}%)")
print(f" Equivalent to ~{distance_savings/40000:.1f} trips around Earth!")
# Create calendar views using ACTUAL optimized weeks
opt_weeks, real_weeks = create_calendar_view_fixed(
optimized_calendar,
optimized_weeks,
real_calendar,
optimized_distance,
real_distance,
)
# Create Gantt chart using ACTUAL optimized weeks
print(f"\nCreating calendar visualizations...")
gantt_fig = create_gantt_chart_fixed(
optimized_calendar, real_calendar, opt_weeks, real_weeks
)
# Create maps
print(f"\nCreating interactive route maps...")
# Create maps using existing functions (these don't need weeks)
opt_map = create_improved_folium_map(
optimized_calendar,
CIRCUIT_COORDINATES,
f"MP Optimized Route ({optimized_distance:,.0f} km)",
"green",
)
real_races = [race for race, date in real_calendar]
real_map = create_improved_folium_map(
real_races,
CIRCUIT_COORDINATES,
f"Real 2026 F1 Calendar ({real_distance:,.0f} km)",
"red",
)
return opt_map, real_map, gantt_fig, opt_weeks, real_weeks
def demonstrate_mp_optimization_fixed():
"""Main function with proper week handling"""
# Step 1: Create distance matrix
print("Step 1: Computing distance matrix...")
distance_matrix = create_distance_matrix()
# Step 2: Calculate real calendar distance
print("Step 2: Analyzing real 2026 F1 calendar...")
real_races = [race for race, date in REAL_2026_CALENDAR]
real_distance = calculate_total_distance(real_races, distance_matrix)
print(f" Real 2026 calendar distance: {real_distance:,.0f} km")
# Step 3: Solve MP optimization model (now returns weeks too)
print("Step 3: Solving MP optimization model...")
optimized_calendar, optimized_weeks, optimized_distance = solve_mp_model(
distance_matrix
)
if optimized_calendar is None:
print("Optimization failed!")
return
print(f"Optimization successful!")
print(f" Optimized weeks: {optimized_weeks}")
print(f" Week range: {min(optimized_weeks)} to {max(optimized_weeks)}")
# Step 4: Create visualizations using ACTUAL weeks
print("Step 4: Creating comparison visualizations...")
results = create_comparison_visualization_fixed(
optimized_calendar,
optimized_weeks,
optimized_distance,
REAL_2026_CALENDAR,
real_distance,
distance_matrix,
)
opt_map, real_map, gantt_fig, opt_weeks_used, real_weeks_used = results
# Step 5: Detailed analysis using ACTUAL weeks
print("Step 5: Performing detailed analysis...")
comparison_df = analyze_calendars_fixed(
optimized_calendar, optimized_weeks, REAL_2026_CALENDAR, distance_matrix
)
return {
"optimized_calendar": optimized_calendar,
"optimized_weeks": optimized_weeks, # Now includes actual optimized weeks
"optimized_distance": optimized_distance,
"real_distance": real_distance,
"savings_km": real_distance - optimized_distance,
"savings_percent": (real_distance - optimized_distance) / real_distance * 100,
"comparison_df": comparison_df,
"opt_weeks": opt_weeks_used,
"real_weeks": real_weeks_used,
"gantt_fig": gantt_fig,
"opt_map": opt_map,
"real_map": real_map,
}
# ========================================
# 8. EXECUTE THE DEMONSTRATION
# ========================================
# Execute the full demonstration
results = demonstrate_mp_optimization_fixed()
if results:
print(f"\n SUCCESS! The MP model demonstrates how mathematical programming")
print(f" with logical constraints optimizes complex real-world problems!")
print(f"\n KEY RESULTS:")
print(
f" • Distance savings: {results['savings_km']:,.0f} km ({results['savings_percent']:.1f}%)"
)
print(f" • MP logical constraints (<==> syntax) successfully applied")
print(f" • Complex scheduling constraints satisfied")
print(f" • Real-world validation completed")
print(f"\n This demonstrates AMPL's MP capabilities for:")
print(f" ✓ Advanced logical constraint modeling")
print(f" ✓ Complex combinatorial optimization")
print(f" ✓ Real-world scheduling applications")
print(f" ✓ Geographic optimization problems")
Step 1: Computing distance matrix...
Computing geodesic distances...
Step 2: Analyzing real 2026 F1 calendar...
Real 2026 calendar distance: 105,540 km
Step 3: Solving MP optimization model...
==================================================
SOLVING WITH AMPL MP LOGICAL CONSTRAINTS
==================================================
Solving with MP logical constraints...
Model features:
• 24 races, 24 positions
• 645 decision variables
• Logical constraints with <==> operators
• Complex clustering and timing constraints
Gurobi 13.0.0: lim:time = 600
Set parameter LogToConsole to value 1
tech:outlev = 1
AMPL MP initial flat model has 551 variables (25 integer, 526 binary);
Objectives: 1 quadratic;
Constraints: 128 linear;
Logical expressions: 82 conditional (in)equalitie(s); 20 and; 62 not; 134 or;
AMPL MP final model has 1016 variables (204 integer, 810 binary);
Objectives: 1 quadratic;
Constraints: 212 linear;
Logical expressions: 20 and; 22 indeq; 41 indge; 61 indle; 134 or;
Set parameter InfUnbdInfo to value 1
Gurobi Optimizer version 13.0.0 build v13.0.0rc1 (linux64 - "Ubuntu 22.04.5 LTS")
CPU model: Intel(R) Xeon(R) CPU @ 2.20GHz, instruction set [SSE2|AVX|AVX2]
Thread count: 1 physical cores, 2 logical processors, using up to 2 threads
Non-default parameters:
TimeLimit 600
InfUnbdInfo 1
Optimize a model with 212 rows, 1016 columns and 1365 nonzeros (Min)
Model fingerprint: 0x06ffcd99
Model has 44 linear objective coefficients
Model has 9702 quadratic objective terms
Model has 278 simple general constraints
20 AND, 134 OR, 124 INDICATOR
Variable types: 262 continuous, 754 integer (0 binary)
Coefficient statistics:
Matrix range [1e+00, 1e+00]
Objective range [3e+02, 2e+04]
QObjective range [2e+02, 4e+04]
Bounds range [1e+00, 5e+01]
RHS range [1e+00, 2e+00]
GenCon rhs range [1e+00, 5e+01]
GenCon coe range [1e+00, 1e+00]
Presolve removed 168 rows and 532 columns
Presolve time: 0.23s
Presolved: 9746 rows, 10186 columns, 30074 nonzeros
Variable types: 0 continuous, 10186 integer (10186 binary)
Found heuristic solution: objective 115619.24800
Found heuristic solution: objective 109482.46780
Root relaxation: objective 6.492109e+03, 298 iterations, 0.05 seconds (0.03 work units)
Nodes | Current Node | Objective Bounds | Work
Expl Unexpl | Obj Depth IntInf | Incumbent BestBd Gap | It/Node Time
0 0 6492.10876 0 103 109482.468 6492.10876 94.1% - 0s
H 0 0 109062.97421 6492.10876 94.0% - 0s
H 0 0 107635.76788 6492.10876 94.0% - 0s
H 0 0 105424.37252 6492.10876 93.8% - 0s
H 0 0 101831.42964 6492.10876 93.6% - 0s
0 0 9294.51925 0 85 101831.430 9294.51925 90.9% - 1s
0 0 11030.8154 0 88 101831.430 11030.8154 89.2% - 1s
0 0 11971.6725 0 66 101831.430 11971.6725 88.2% - 1s
0 0 12115.5596 0 63 101831.430 12115.5596 88.1% - 2s
0 0 12151.3133 0 63 101831.430 12151.3133 88.1% - 2s
0 0 12151.3133 0 57 101831.430 12151.3133 88.1% - 2s
0 2 12907.1503 0 57 101831.430 12907.1503 87.3% - 2s
H 2 2 100489.32774 12907.1503 87.2% 42.0 2s
H 4 4 100475.68288 12907.1503 87.2% 131 2s
H 93 93 91026.482786 13828.8832 84.8% 96.6 4s
H 93 93 88895.242988 13828.8832 84.4% 96.6 4s
H 93 93 85757.654260 13828.8832 83.9% 96.6 5s
H 94 94 85384.545472 13828.8832 83.8% 99.1 5s
H 94 94 84917.200909 13828.8832 83.7% 99.1 5s
H 94 94 84469.722617 13828.8832 83.6% 99.1 5s
385 369 38141.8420 67 58 84469.7226 13937.9037 83.5% 88.0 10s
H 548 476 80864.526196 13941.6260 82.8% 77.0 13s
558 482 22032.5236 87 104 80864.5262 14203.1428 82.4% 75.6 15s
H 560 460 80783.855851 14286.5013 82.3% 75.3 15s
H 580 448 79818.704541 14932.2001 81.3% 72.7 17s
H 580 425 78883.738127 14932.2001 81.1% 72.7 18s
H 580 403 72800.182049 14932.2001 79.5% 72.7 19s
H 580 382 72501.758480 14932.2001 79.4% 72.7 19s
H 580 362 70942.410723 14932.2001 79.0% 72.7 19s
H 580 344 70940.003788 14932.2001 79.0% 72.7 21s
614 367 19169.3036 19 124 70940.0038 15576.2719 78.0% 68.7 25s
648 391 18632.7893 21 78 70940.0038 16421.7405 76.9% 92.9 30s
720 439 33975.3060 57 93 70940.0038 16421.7405 76.9% 107 35s
H 785 460 69724.549000 16421.7405 76.4% 120 40s
884 524 65401.7032 139 53 69724.5490 16421.7405 76.4% 122 45s
980 566 31299.0209 44 77 69724.5490 16421.7405 76.4% 132 50s
1039 605 32776.4773 74 92 69724.5490 16421.7405 76.4% 143 55s
1130 666 42795.7634 119 91 69724.5490 16421.7405 76.4% 148 60s
1247 727 32479.2413 21 79 69724.5490 16421.7405 76.4% 146 65s
1321 776 36616.0935 58 77 69724.5490 16421.7405 76.4% 158 70s
1420 842 57687.2609 108 92 69724.5490 16421.7405 76.4% 167 75s
1503 874 33196.5048 30 80 69724.5490 16421.7405 76.4% 169 80s
1621 953 38129.5796 89 87 69724.5490 16421.7405 76.4% 177 85s
1763 1043 16582.6127 26 105 69724.5490 16421.7405 76.4% 173 90s
1851 1131 32533.7168 70 94 69724.5490 16421.7405 76.4% 176 95s
1946 1226 43165.6423 117 125 69724.5490 16421.7405 76.4% 179 100s
2018 1298 57788.7644 153 136 69724.5490 16421.7405 76.4% 178 105s
2139 1388 43409.3479 46 96 69724.5490 16570.7589 76.2% 180 110s
2279 1498 35371.5493 27 102 69724.5490 16894.8144 75.8% 182 115s
2367 1586 37202.2487 71 124 69724.5490 16894.8144 75.8% 185 120s
2495 1713 68964.0069 135 105 69724.5490 16894.8144 75.8% 185 125s
2618 1805 37136.9265 43 106 69724.5490 16965.0043 75.7% 185 130s
2765 1941 17979.7797 24 92 69724.5490 17374.7744 75.1% 183 135s
2938 2112 62552.6019 148 74 69724.5490 17374.7744 75.1% 180 140s
3046 2203 43226.3760 77 87 69724.5490 17375.5246 75.1% 180 145s
3210 2346 42096.4720 59 90 69724.5490 17466.7127 74.9% 176 150s
3367 2503 51783.7145 174 106 69724.5490 17466.7127 74.9% 173 155s
H 3499 2613 69704.460904 17480.9789 74.9% 172 160s
3641 2741 34614.6316 62 124 69704.4609 17524.0288 74.9% 171 165s
3761 2861 55422.5030 134 122 69704.4609 17524.0288 74.9% 170 170s
3922 3003 60551.0130 99 119 69704.4609 17554.9501 74.8% 169 175s
4087 3141 36656.5551 33 103 69704.4609 17554.9501 74.8% 168 180s
4230 3272 44682.9815 45 90 69704.4609 17561.0961 74.8% 167 185s
4442 3466 41945.4543 56 103 69704.4609 17576.6859 74.8% 164 190s
4605 3592 19781.1265 18 71 69704.4609 17607.0463 74.7% 162 195s
4809 3773 34197.5453 19 79 69704.4609 17614.3987 74.7% 158 200s
H 4897 3859 69656.274658 17614.3987 74.7% 158 202s
4957 3904 49930.5069 39 120 69656.2747 17614.3987 74.7% 157 205s
5101 4023 55806.1479 27 79 69656.2747 17688.8252 74.6% 156 210s
5409 4240 37716.4691 29 77 69656.2747 17911.2011 74.3% 151 215s
5611 4401 59674.0445 71 90 69656.2747 17911.2011 74.3% 149 220s
5856 4594 39042.3226 32 87 69656.2747 17950.5227 74.2% 147 225s
H 5934 4640 68772.101249 17979.7797 73.9% 146 227s
6059 4741 52405.8498 27 94 68772.1012 17979.7797 73.9% 145 230s
H 6085 4726 68149.556206 17979.7797 73.6% 145 230s
6197 4813 33361.6867 29 88 68149.5562 18002.0615 73.6% 147 235s
6413 4965 47804.5120 43 97 68149.5562 18258.9052 73.2% 146 240s
6624 5156 67550.1740 114 63 68149.5562 18264.4008 73.2% 145 245s
6866 5358 48709.6773 71 95 68149.5562 18340.2771 73.1% 144 250s
7087 5540 55819.5952 36 83 68149.5562 18583.3031 72.7% 142 255s
7281 5658 43131.0692 46 93 68149.5562 18955.0272 72.2% 141 260s
7502 5853 39335.3310 43 78 68149.5562 18980.4080 72.1% 141 265s
7674 5999 47031.4607 75 94 68149.5562 19434.4447 71.5% 141 270s
H 7873 6080 67070.695189 19539.6206 70.9% 140 276s
8054 6233 57350.3488 33 62 67070.6952 19587.6813 70.8% 139 280s
8252 6372 32924.2763 26 87 67070.6952 19640.0809 70.7% 138 285s
8526 6598 66722.7444 104 126 67070.6952 19673.0832 70.7% 137 290s
8769 6800 53313.0141 61 103 67070.6952 19767.9789 70.5% 137 295s
8856 6864 50716.5760 58 88 67070.6952 19781.1265 70.5% 137 300s
9081 7037 57039.1143 53 81 67070.6952 19938.8428 70.3% 136 305s
9327 7244 59121.1606 68 86 67070.6952 19997.2062 70.2% 135 310s
9558 7436 66752.6453 69 64 67070.6952 20067.2446 70.1% 135 315s
9839 7665 59096.9669 109 126 67070.6952 20728.6024 69.1% 134 324s
9850 7669 63363.7288 114 104 67070.6952 20728.6024 69.1% 134 325s
10089 7873 66491.5581 94 90 67070.6952 20818.5153 69.0% 134 330s
H10098 7869 67060.782143 21037.2380 68.6% 134 336s
10132 7900 64826.6364 58 50 67060.7821 21037.2380 68.6% 134 340s
H10152 7890 66807.694982 21039.2380 68.5% 134 340s
H10152 7883 66660.775628 21039.2380 68.4% 134 340s
H10152 7872 66551.864498 21039.2380 68.4% 134 340s
H10152 7867 66526.143074 21039.2380 68.4% 134 340s
10203 7911 48838.5607 70 4873 66526.1431 21064.0151 68.3% 134 396s
10223 7924 36064.7608 46 327 66526.1431 26709.5185 59.9% 134 400s
10246 7940 43318.3365 70 412 66526.1431 31606.4457 52.5% 134 405s
10269 7955 47865.4752 102 378 66526.1431 33284.2854 50.0% 133 410s
10285 7966 35933.2550 28 374 66526.1431 33670.9426 49.4% 133 415s
10307 7980 41006.9348 62 407 66526.1431 33970.0071 48.9% 133 420s
10325 7992 61572.6572 37 444 66526.1431 34187.4499 48.6% 133 425s
10338 8001 44385.3697 60 351 66526.1431 34264.4782 48.5% 132 430s
10357 8014 44446.4785 61 417 66526.1431 34339.5466 48.4% 132 435s
10376 8026 42331.5536 49 457 66526.1431 34433.5390 48.2% 132 440s
10392 8037 65216.2310 134 444 66526.1431 34485.0020 48.2% 132 445s
10409 8048 46281.5028 63 480 66526.1431 34528.8752 48.1% 131 451s
10418 8054 35614.7406 43 497 66526.1431 34540.9598 48.1% 131 455s
10430 8062 34556.0140 40 496 66526.1431 34556.0140 48.1% 131 460s
10444 8072 58369.0874 104 474 66526.1431 34576.3961 48.0% 131 465s
10452 8077 50563.2718 42 481 66526.1431 34589.2581 48.0% 131 470s
10459 8084 38603.0930 29 234 66526.1431 34750.8111 47.8% 134 475s
10475 8094 38812.8929 37 261 66526.1431 34750.8111 47.8% 134 480s
10493 8106 42736.1474 46 146 66526.1431 34750.8111 47.8% 135 485s
10517 8122 47583.5195 58 175 66526.1431 34750.8111 47.8% 135 490s
10555 8148 58406.2952 77 138 66526.1431 34750.8111 47.8% 136 495s
10615 8142 41849.9239 30 316 66526.1431 34750.8111 47.8% 136 500s
10627 8150 41533.9564 36 280 66526.1431 34750.8111 47.8% 136 505s
10643 8160 42177.0522 44 252 66526.1431 34750.8111 47.8% 137 510s
10661 8172 46042.1343 53 216 66526.1431 34750.8111 47.8% 138 515s
10678 8184 44342.2432 62 275 66526.1431 34750.8111 47.8% 138 520s
10699 8198 47050.4689 72 249 66526.1431 34750.8111 47.8% 139 525s
H10713 7796 66525.341071 34750.8111 47.8% 139 528s
10719 7802 56513.9094 82 172 66525.3411 34750.8111 47.8% 139 530s
10755 7818 57135.2305 100 166 66525.3411 34750.8111 47.8% 140 535s
10808 7839 46179.8696 33 225 66525.3411 36485.4557 45.2% 140 540s
10826 7851 47445.0153 42 207 66525.3411 36485.4557 45.2% 141 545s
10848 7866 50182.3608 53 167 66525.3411 36485.4557 45.2% 141 550s
H10869 7491 66485.110458 36485.4557 45.1% 142 554s
10869 7493 52850.8893 64 151 66485.1105 36485.4557 45.1% 142 556s
10901 7502 44428.1729 43 193 66485.1105 36486.7455 45.1% 142 560s
10947 7527 62208.8445 66 123 66485.1105 36486.7455 45.1% 143 565s
10980 7533 37503.0815 39 203 66485.1105 36620.4243 44.9% 143 570s
11006 7550 38745.8811 52 211 66485.1105 36620.4243 44.9% 144 575s
11031 7567 40353.6273 64 195 66485.1105 36620.4243 44.9% 144 580s
11072 7594 58508.8245 85 138 66485.1105 36620.4243 44.9% 145 585s
11124 7603 41535.8000 39 245 66485.1105 36620.4481 44.9% 145 590s
11148 7619 48667.8939 51 216 66485.1105 36620.4481 44.9% 145 595s
11175 7637 41341.9978 65 180 66485.1105 36620.4481 44.9% 146 600s
Cutting planes:
Gomory: 2
Lift-and-project: 1
Cover: 40
Dual implied bound: 31
MIR: 10
Flow cover: 1472
GUB cover: 2
Zero half: 60
RLT: 402
Relax-and-lift: 2
Explored 11176 nodes (1633161 simplex iterations) in 600.00 seconds (430.13 work units)
Thread count was 2 (of 2 available processors)
Solution count 10: 66485.1 66525.3 66526.1 ... 68772.1
Time limit reached
Best objective 6.648511045774e+04, best bound 3.662044813713e+04, gap 44.9193%
Gurobi 13.0.0: time limit, feasible solution; objective 66485.11046
1.63316e+06 simplex iterations
11176 branching nodes
absmipgap=29864.7, relmipgap=0.449193
Solver status: limit
Optimized total distance: 66,485 km
Optimization successful!
Optimized weeks: [10, 11, 13, 15, 16, 18, 19, 21, 22, 24, 26, 28, 34, 35, 36, 38, 39, 40, 42, 43, 45, 46, 48, 49]
Week range: 10 to 49
Step 4: Creating comparison visualizations...
============================================================
OPTIMIZATION vs REAL 2026 CALENDAR COMPARISON
============================================================
OPTIMIZED SOLUTION (MP Model):
Total distance: 66,485 km
REAL 2026 F1 CALENDAR:
Total distance: 105,540 km
OPTIMIZATION BENEFIT:
Distance saved: 39,055 km (37.0%)
Equivalent to ~1.0 trips around Earth!
WEEKLY CALENDAR COMPARISON
================================================================================
OPTIMIZED SCHEDULE (Total: 66,485 km)
--------------------------------------------------------------------------------
Week | Optimized Route | Real 2026 Calendar | Notes
--------------------------------------------------------------------------------
10 | Australia | Australia | Same
11 | Singapore | China | Different
13 | Japan | Japan | Same
15 | Las Vegas | Bahrain | Different
16 | Mexico | Saudi Arabia | Different
18 | Austin | Miami | Different
19 | Canada | | Opt only
21 | Miami | Canada | Different
22 | Brazil | | Opt only
23 | | Monaco | Real only
24 | Spain Madrid | Spain Barcelona | Different
26 | Spain Barcelona | Austria | Different
27 | | Great Britain | Real only
28 | Monaco | | Opt only
29 | | Belgium | Real only
30 | | Hungary | Real only
34 | Italy | Netherlands | Different
35 | Great Britain | | Opt only
36 | Netherlands | Italy | Different
37 | | Spain Madrid | Real only
38 | Belgium | | Opt only
39 | Austria | Azerbaijan | Different
40 | Hungary | | Opt only
41 | | Singapore | Real only
42 | Azerbaijan | | Opt only
43 | China | Austin | Different
44 | | Mexico | Real only
45 | Bahrain | Brazil | Different
46 | Saudi Arabia | | Opt only
47 | | Las Vegas | Real only
48 | Qatar | Qatar | Same
49 | Abu Dhabi | Abu Dhabi | Same
--------------------------------------------------------------------------------
TOTAL DISTANCES:
Optimized: 66,485 km
Real 2026: 105,540 km
Savings: 39,055 km ( 37.0%)
Creating calendar visualizations...
Creating interactive route maps...
Step 5: Performing detailed analysis...
DETAILED CALENDAR ANALYSIS WITH ACTUAL WEEKS
======================================================================
Pos | Optimized (Week) | Real 2026 (Week) | Week Δ | Dist Δ
----------------------------------------------------------------------
1 | Australia (W10) | Australia (W10) | -- | Start
2 | Singapore (W11) | China (W11) | = | ↑ 2021
3 | Japan (W13) | Japan (W13) | = | ↓ 3558
4 | Las Vegas (W15) | Bahrain (W15) | = | ↓ 1131
5 | Mexico (W16) | Saudi Arabia (W16) | = | ↓ 1175
6 | Austin (W18) | Miami (W18) | = | ↑ 10404
7 | Canada (W19) | Canada (W21) | +2 | ↓ 449
8 | Miami (W21) | Monaco (W23) | +2 | ↑ 3868
9 | Brazil (W22) | Spain Barcelona (W24) | +2 | ↓ 6111
10 | Spain Madrid (W24) | Austria (W26) | +2 | ↓ 7229
11 | Spain Barcelona (W26) | Great Britain (W27) | +1 | ↑ 738
12 | Monaco (W28) | Belgium (W29) | +1 | ↑ 34
13 | Italy (W34) | Hungary (W30) | -4 | ↑ 762
14 | Great Britain (W35) | Netherlands (W34) | -1 | ↑ 137
15 | Netherlands (W36) | Italy (W36) | = | ↑ 448
16 | Belgium (W38) | Spain Madrid (W37) | -1 | ↑ 963
17 | Austria (W39) | Azerbaijan (W39) | = | ↑ 3727
18 | Hungary (W40) | Singapore (W41) | +1 | ↑ 6607
19 | Azerbaijan (W42) | Austin (W43) | +1 | ↑ 13286
20 | China (W43) | Mexico (W44) | +1 | ↓ 5135
21 | Bahrain (W45) | Brazil (W45) | = | ↑ 626
22 | Saudi Arabia (W46) | Las Vegas (W47) | +1 | ↑ 8529
23 | Qatar (W48) | Qatar (W48) | = | ↑ 11693
24 | Abu Dhabi (W49) | Abu Dhabi (W49) | = | = 0
WEEK ANALYSIS:
Optimized calendar spans weeks 10 to 49
Real calendar spans weeks 10 to 49
Summer break (weeks 31-33):
Optimized: Respected
Real 2026: Respected
Consecutive week transitions:
Optimized: 11 consecutive transitions
Real 2026: 10 consecutive transitions
SUCCESS! The MP model demonstrates how mathematical programming
with logical constraints optimizes complex real-world problems!
KEY RESULTS:
• Distance savings: 39,055 km (37.0%)
• MP logical constraints (<==> syntax) successfully applied
• Complex scheduling constraints satisfied
• Real-world validation completed
This demonstrates AMPL's MP capabilities for:
✓ Advanced logical constraint modeling
✓ Complex combinatorial optimization
✓ Real-world scheduling applications
✓ Geographic optimization problems
# ========================================
# 9. DISPLAY INTERACTIVE VISUALIZATIONS
# ========================================
if results:
# Display static comparison map
# comparison_fig = create_dual_map_comparison(
# results['optimized_calendar'],
# REAL_2026_CALENDAR,
# CIRCUIT_COORDINATES,
# results['optimized_distance'],
# results['real_distance']
# )
# plt.show()
# Display interactive maps
display(HTML("<h3>🎯 MP Optimized Route (Green)</h3>"))
display(results["opt_map"])
print(f"\n🏁 MP MODEL DEMONSTRATION COMPLETE!")
print("=" * 50)
print(f"✅ Successfully used AMPL MP logical constraints (<==> syntax)")
print(f"✅ Optimized complex scheduling with multiple constraint types")
print(f"✅ Achieved {results['savings_percent']:.1f}% distance reduction")
print(f"✅ Created comprehensive calendar and route visualizations")
print(f"✅ Demonstrated real-world application of mathematical programming")
🎯 MP Optimized Route (Green)
🏁 MP MODEL DEMONSTRATION COMPLETE!
==================================================
✅ Successfully used AMPL MP logical constraints (<==> syntax)
✅ Optimized complex scheduling with multiple constraint types
✅ Achieved 37.0% distance reduction
✅ Created comprehensive calendar and route visualizations
✅ Demonstrated real-world application of mathematical programming
display(HTML("<h3>📅 Real 2026 F1 Calendar (Red)</h3>"))
display(results["real_map"])