arre99 commited on
Commit
0f5d5f3
·
1 Parent(s): fa395d9

cleaned up track visualization by removing the driver and automatically picking the fastest lap for the race. Now both speed and gear have a nice legend that is under the image

Browse files
Files changed (6) hide show
  1. README.md +1 -1
  2. api_playground.ipynb +112 -0
  3. app.py +3 -4
  4. fastf1_tools.py +2 -3
  5. utils/constants.py +9 -0
  6. utils/track_utils.py +36 -32
README.md CHANGED
@@ -8,7 +8,7 @@ sdk_version: 5.32.0
8
  app_file: app.py
9
  pinned: true
10
  license: apache-2.0
11
- short_description: 'Historical & real-time F1 data and strategy'
12
  video_url: TBD
13
  tags:
14
  - 'mcp-server-track'
 
8
  app_file: app.py
9
  pinned: true
10
  license: apache-2.0
11
+ short_description: 'Historical & real-time F1 data retrieval with agentic race strategy capabilities'
12
  video_url: TBD
13
  tags:
14
  - 'mcp-server-track'
api_playground.ipynb CHANGED
@@ -32,6 +32,118 @@
32
  "# FastF1"
33
  ]
34
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  {
36
  "cell_type": "code",
37
  "execution_count": 2,
 
32
  "# FastF1"
33
  ]
34
  },
35
+ {
36
+ "cell_type": "code",
37
+ "execution_count": 7,
38
+ "id": "2a205292",
39
+ "metadata": {},
40
+ "outputs": [
41
+ {
42
+ "name": "stderr",
43
+ "output_type": "stream",
44
+ "text": [
45
+ "core INFO \tLoading data for Australian Grand Prix - Race [v3.5.3]\n",
46
+ "req INFO \tUsing cached data for session_info\n",
47
+ "req INFO \tUsing cached data for driver_info\n",
48
+ "req INFO \tUsing cached data for session_status_data\n",
49
+ "req INFO \tUsing cached data for lap_count\n",
50
+ "req INFO \tUsing cached data for track_status_data\n",
51
+ "req INFO \tUsing cached data for _extended_timing_data\n",
52
+ "req INFO \tUsing cached data for timing_app_data\n",
53
+ "core INFO \tProcessing timing data...\n",
54
+ "req INFO \tUsing cached data for car_data\n",
55
+ "req INFO \tUsing cached data for position_data\n",
56
+ "req INFO \tUsing cached data for weather_data\n",
57
+ "req INFO \tUsing cached data for race_control_messages\n",
58
+ "core WARNING \tDriver 4 completed the race distance 00:00.022000 before the recorded end of the session.\n",
59
+ "core INFO \tFinished loading data for 20 drivers: ['4', '1', '63', '12', '23', '18', '27', '16', '81', '44', '10', '22', '31', '87', '30', '5', '14', '55', '7', '6']\n"
60
+ ]
61
+ },
62
+ {
63
+ "name": "stdout",
64
+ "output_type": "stream",
65
+ "text": [
66
+ "Index(['Time', 'Driver', 'DriverNumber', 'LapTime', 'LapNumber', 'Stint',\n",
67
+ " 'PitOutTime', 'PitInTime', 'Sector1Time', 'Sector2Time', 'Sector3Time',\n",
68
+ " 'Sector1SessionTime', 'Sector2SessionTime', 'Sector3SessionTime',\n",
69
+ " 'SpeedI1', 'SpeedI2', 'SpeedFL', 'SpeedST', 'IsPersonalBest',\n",
70
+ " 'Compound', 'TyreLife', 'FreshTyre', 'Team', 'LapStartTime',\n",
71
+ " 'LapStartDate', 'TrackStatus', 'Position', 'Deleted', 'DeletedReason',\n",
72
+ " 'FastF1Generated', 'IsAccurate'],\n",
73
+ " dtype='object')\n",
74
+ " Driver DriverNumber Team\n",
75
+ "0 VER 1 Red Bull Racing\n",
76
+ "1 VER 1 Red Bull Racing\n",
77
+ "2 VER 1 Red Bull Racing\n",
78
+ "3 VER 1 Red Bull Racing\n",
79
+ "4 VER 1 Red Bull Racing\n",
80
+ ".. ... ... ...\n",
81
+ "922 BEA 87 Haas F1 Team\n",
82
+ "923 BEA 87 Haas F1 Team\n",
83
+ "924 BEA 87 Haas F1 Team\n",
84
+ "925 BEA 87 Haas F1 Team\n",
85
+ "926 BEA 87 Haas F1 Team\n",
86
+ "\n",
87
+ "[927 rows x 3 columns]\n"
88
+ ]
89
+ }
90
+ ],
91
+ "source": [
92
+ "session = fastf1.get_session(2025, 1, \"race\")\n",
93
+ "session.load()\n",
94
+ "print(session.laps.columns)\n",
95
+ "print(session.laps[[\"Driver\", \"DriverNumber\", \"Team\"]])"
96
+ ]
97
+ },
98
+ {
99
+ "cell_type": "code",
100
+ "execution_count": 5,
101
+ "id": "44f7516f",
102
+ "metadata": {},
103
+ "outputs": [
104
+ {
105
+ "name": "stdout",
106
+ "output_type": "stream",
107
+ "text": [
108
+ "Time 0 days 02:28:03.586000\n",
109
+ "Driver NOR\n",
110
+ "DriverNumber 4\n",
111
+ "LapTime 0 days 00:01:22.167000\n",
112
+ "LapNumber 43.0\n",
113
+ "Stint 5.0\n",
114
+ "PitOutTime NaT\n",
115
+ "PitInTime NaT\n",
116
+ "Sector1Time 0 days 00:00:28.553000\n",
117
+ "Sector2Time 0 days 00:00:18.537000\n",
118
+ "Sector3Time 0 days 00:00:35.077000\n",
119
+ "Sector1SessionTime 0 days 02:27:10.030000\n",
120
+ "Sector2SessionTime 0 days 02:27:28.567000\n",
121
+ "Sector3SessionTime 0 days 02:28:03.644000\n",
122
+ "SpeedI1 271.0\n",
123
+ "SpeedI2 290.0\n",
124
+ "SpeedFL 292.0\n",
125
+ "SpeedST 300.0\n",
126
+ "IsPersonalBest True\n",
127
+ "Compound HARD\n",
128
+ "TyreLife 10.0\n",
129
+ "FreshTyre False\n",
130
+ "Team McLaren\n",
131
+ "LapStartTime 0 days 02:26:41.419000\n",
132
+ "LapStartDate 2025-03-16 05:34:04.038000\n",
133
+ "TrackStatus 1\n",
134
+ "Position 1.0\n",
135
+ "Deleted False\n",
136
+ "DeletedReason \n",
137
+ "FastF1Generated False\n",
138
+ "IsAccurate True\n",
139
+ "Name: 635, dtype: object\n"
140
+ ]
141
+ }
142
+ ],
143
+ "source": [
144
+ "print(session.laps.pick_fastest())"
145
+ ]
146
+ },
147
  {
148
  "cell_type": "code",
149
  "execution_count": 2,
app.py CHANGED
@@ -91,11 +91,10 @@ iface_track_visualization = gr.Interface(
91
  gr.Number(label="Calendar year", value=CURRENT_YEAR, minimum=1950, maximum=CURRENT_YEAR),
92
  gr.Textbox(label="Grand Prix", placeholder="Ex: Monaco", info="The name of the GP/country/location (Fuzzy matching supported) or round number"),
93
  gr.Radio(["speed", "corners", "gear"], label="Visualization type", value="speed", info="What type of track visualization to generate"),
94
- gr.Dropdown(label="Driver", choices=DRIVER_NAMES, info="Only applied for speed visualization. gear uses fastest lap during race.")
95
  ],
96
  outputs="image",
97
  title="Track Visualizations",
98
- description="Get the track visualization for the given Grand Prix. Example: (2025,Monaco,speed,Leclerc)"
99
  )
100
 
101
  iface_session_results = gr.Interface(
@@ -121,7 +120,7 @@ iface_driver_info = gr.Interface(
121
  ],
122
  outputs="text",
123
  title="Driver Info",
124
- description="Get background information about a specific driver"
125
  )
126
 
127
  iface_constructor_info = gr.Interface(
@@ -131,7 +130,7 @@ iface_constructor_info = gr.Interface(
131
  ],
132
  outputs="text",
133
  title="Constructor Info",
134
- description="Get background information about a specific constructor"
135
  )
136
 
137
 
 
91
  gr.Number(label="Calendar year", value=CURRENT_YEAR, minimum=1950, maximum=CURRENT_YEAR),
92
  gr.Textbox(label="Grand Prix", placeholder="Ex: Monaco", info="The name of the GP/country/location (Fuzzy matching supported) or round number"),
93
  gr.Radio(["speed", "corners", "gear"], label="Visualization type", value="speed", info="What type of track visualization to generate"),
 
94
  ],
95
  outputs="image",
96
  title="Track Visualizations",
97
+ description="Get the track visualization (speed/corners/gear) for the given Grand Prix race. Example: (2025,Monaco,speed)"
98
  )
99
 
100
  iface_session_results = gr.Interface(
 
120
  ],
121
  outputs="text",
122
  title="Driver Info",
123
+ description="Get background information about a specific driver from the 2025 Formula 1 season"
124
  )
125
 
126
  iface_constructor_info = gr.Interface(
 
130
  ],
131
  outputs="text",
132
  title="Constructor Info",
133
+ description="Get background information about a specific constructor from the 2025 Formula 1 season"
134
  )
135
 
136
 
fastf1_tools.py CHANGED
@@ -155,14 +155,13 @@ def constructor_championship_standings(year: int, constructor_name: str) -> str:
155
  standings_string = f"{constructor_name} {are_were} {constructor_standing['position'].iloc[0]}{suffix} with {constructor_standing['points'].iloc[0]} points and {constructor_standing['wins'].iloc[0]} wins"
156
  return standings_string
157
 
158
- def track_visualization(year: int, round: gp, visualization_type: str, driver_name: str) -> Image.Image:
159
  """Generate a visualization of the track with specified data.
160
 
161
  Args:
162
  year (int): The season year
163
  round (str | int): The race round number or name
164
  visualization_type (str): Type of visualization ('speed', 'corners', or 'gear')
165
- driver_name (str): Name of the driver for driver-specific visualizations
166
 
167
  Returns:
168
  Image.Image: A PIL Image object containing the visualization
@@ -174,7 +173,7 @@ def track_visualization(year: int, round: gp, visualization_type: str, driver_na
174
  session.load()
175
 
176
  if visualization_type == "speed":
177
- return track_utils.create_track_speed_visualization(session, driver_name)
178
  elif visualization_type == "corners":
179
  return track_utils.create_track_corners_visualization(session)
180
  elif visualization_type == "gear":
 
155
  standings_string = f"{constructor_name} {are_were} {constructor_standing['position'].iloc[0]}{suffix} with {constructor_standing['points'].iloc[0]} points and {constructor_standing['wins'].iloc[0]} wins"
156
  return standings_string
157
 
158
+ def track_visualization(year: int, round: gp, visualization_type: str) -> Image.Image:
159
  """Generate a visualization of the track with specified data.
160
 
161
  Args:
162
  year (int): The season year
163
  round (str | int): The race round number or name
164
  visualization_type (str): Type of visualization ('speed', 'corners', or 'gear')
 
165
 
166
  Returns:
167
  Image.Image: A PIL Image object containing the visualization
 
173
  session.load()
174
 
175
  if visualization_type == "speed":
176
+ return track_utils.create_track_speed_visualization(session)
177
  elif visualization_type == "corners":
178
  return track_utils.create_track_corners_visualization(session)
179
  elif visualization_type == "gear":
utils/constants.py CHANGED
@@ -118,26 +118,35 @@ The implemented functions make it possible to:
118
  - Apply filters to an API string - `apply_filters(api_string, *filters)`
119
  - Send a request to the OpenF1 API - `send_request(api_string)`
120
 
 
 
121
  """
122
 
 
123
  MARKDOWN_OPENF1_EXAMPLES = """
124
 
125
  ' Retrieve data about car number 55 with session_key 9159 where the speed was greater than 315 '
 
126
  ```https://api.openf1.org/v1/car_data?driver_number=55&session_key=9159&speed>=315```
127
 
128
  ' Retrieve data about driver number 1 with session_key 9158 '
 
129
  ```https://api.openf1.org/v1/drivers?driver_number=1&session_key=9158```
130
 
131
  ' Retrieve data about intervals with session_key 9165 where the interval was less than 0.005s'
 
132
  ```https://api.openf1.org/v1/intervals?session_key=9165&interval<0.005```
133
 
134
  ' Retrieve data about laps with session_key 9161 for driver number 63 on lap number 8'
 
135
  ```https://api.openf1.org/v1/laps?session_key=9161&driver_number=63&lap_number=8```
136
 
137
  ' Retrieve data about meetings in 2023 for Singapore'
 
138
  ```https://api.openf1.org/v1/meetings?year=2023&country_name=Singapore```
139
 
140
  ' Retrieve data about pit stops with session_key 9158 where the pit duration was less than 31s'
 
141
  ```https://api.openf1.org/v1/pit?session_key=9158&pit_duration<31```
142
 
143
  """
 
118
  - Apply filters to an API string - `apply_filters(api_string, *filters)`
119
  - Send a request to the OpenF1 API - `send_request(api_string)`
120
 
121
+ The inputs are strings while the output is a JSON object. The examples are listed in an order that would be expected to be used in a real-life scenario. Some example API strings are listed below in the last tool `send_request(api_string)`.
122
+
123
  """
124
 
125
+
126
  MARKDOWN_OPENF1_EXAMPLES = """
127
 
128
  ' Retrieve data about car number 55 with session_key 9159 where the speed was greater than 315 '
129
+
130
  ```https://api.openf1.org/v1/car_data?driver_number=55&session_key=9159&speed>=315```
131
 
132
  ' Retrieve data about driver number 1 with session_key 9158 '
133
+
134
  ```https://api.openf1.org/v1/drivers?driver_number=1&session_key=9158```
135
 
136
  ' Retrieve data about intervals with session_key 9165 where the interval was less than 0.005s'
137
+
138
  ```https://api.openf1.org/v1/intervals?session_key=9165&interval<0.005```
139
 
140
  ' Retrieve data about laps with session_key 9161 for driver number 63 on lap number 8'
141
+
142
  ```https://api.openf1.org/v1/laps?session_key=9161&driver_number=63&lap_number=8```
143
 
144
  ' Retrieve data about meetings in 2023 for Singapore'
145
+
146
  ```https://api.openf1.org/v1/meetings?year=2023&country_name=Singapore```
147
 
148
  ' Retrieve data about pit stops with session_key 9158 where the pit duration was less than 31s'
149
+
150
  ```https://api.openf1.org/v1/pit?session_key=9158&pit_duration<31```
151
 
152
  """
utils/track_utils.py CHANGED
@@ -2,11 +2,11 @@ import matplotlib as mpl
2
  import numpy as np
3
  from matplotlib import pyplot as plt
4
  from matplotlib.collections import LineCollection
5
- import fastf1 as ff1
6
  from PIL import Image
7
  from io import BytesIO
8
  from typing import Union
9
  import json
 
10
 
11
  # Custom types
12
  gp = Union[str, int]
@@ -19,15 +19,10 @@ def rotate(xy, *, angle):
19
  return np.matmul(xy, rot_mat)
20
 
21
 
22
-
23
- def create_track_speed_visualization(session, driver_name: str) -> Image:
24
 
25
  weekend = session.event
26
- session.load()
27
- with open("assets/driver_abbreviations.json") as f:
28
- driver_abbreviations = json.load(f)
29
- driver_abbreviation = driver_abbreviations[driver_name]
30
- lap = session.laps.pick_drivers(driver_abbreviation).pick_fastest()
31
 
32
  # Get telemetry data
33
  x = lap.telemetry['X'] # values for x-axis
@@ -40,7 +35,7 @@ def create_track_speed_visualization(session, driver_name: str) -> Image:
40
 
41
  # We create a plot with title and adjust some setting to make it look good.
42
  fig, ax = plt.subplots(sharex=True, sharey=True, figsize=(12, 6.75))
43
- fig.suptitle(f'{weekend["EventName"]} - {driver_name} ', size=24, y=0.97)
44
 
45
  # Adjust margins and turn of axis
46
  plt.subplots_adjust(left=0.1, right=0.9, top=0.9, bottom=0.12)
@@ -67,6 +62,7 @@ def create_track_speed_visualization(session, driver_name: str) -> Image:
67
  normlegend = mpl.colors.Normalize(vmin=color.min(), vmax=color.max())
68
  legend = mpl.colorbar.ColorbarBase(cbaxes, norm=normlegend, cmap=mpl.colormaps['viridis'],
69
  orientation="horizontal")
 
70
 
71
  # Create a PIL image from the plot
72
  fig = plt.gcf()
@@ -86,7 +82,7 @@ def create_track_speed_visualization(session, driver_name: str) -> Image:
86
  return img
87
 
88
 
89
- def create_track_corners_visualization(session) -> Image:
90
 
91
  lap = session.laps.pick_fastest()
92
  pos = lap.get_pos_data()
@@ -162,8 +158,8 @@ def create_track_corners_visualization(session) -> Image:
162
  return img
163
 
164
 
165
- def create_track_gear_visualization(session) -> Image:
166
-
167
  lap = session.laps.pick_fastest()
168
  tel = lap.get_telemetry()
169
 
@@ -174,39 +170,47 @@ def create_track_gear_visualization(session) -> Image:
174
  segments = np.concatenate([points[:-1], points[1:]], axis=1)
175
  gear = tel['nGear'].to_numpy().astype(float)
176
 
177
- cmap = plt.cm.get_cmap('viridis', 8)
178
- norm = plt.Normalize(1, 8)
179
- lc_comp = LineCollection(segments, norm=norm, cmap=cmap)
 
 
 
 
 
 
 
 
 
180
  lc_comp.set_array(gear)
181
  lc_comp.set_linewidth(4)
 
182
 
183
- plt.gca().add_collection(lc_comp)
184
- plt.axis('equal')
185
- plt.tick_params(labelleft=False, left=False, labelbottom=False, bottom=False)
 
 
186
 
187
- plt.suptitle(
188
- f"Fastest Lap Gear Shift Visualization\n"
189
- f"{lap['Driver']} - {session.event['EventName']}"
190
- )
191
 
192
- cbar = plt.colorbar(mappable=lc_comp, label="Gear")
193
- cbar.set_ticks(np.arange(1, 9)) # Set ticks at integer positions
194
- cbar.set_ticklabels(np.arange(1, 9)) # Labels from 1 to 8
 
 
 
 
 
195
 
196
  # Create a PIL image from the plot
197
- fig = plt.gcf()
198
-
199
- # Save the figure to a BytesIO buffer and convert to bytes
200
  buf = BytesIO()
201
  fig.savefig(buf, format='png', dpi=150, bbox_inches='tight')
202
  buf.seek(0)
203
-
204
- # Create PIL image from buffer bytes and close the figure
205
  img_data = buf.getvalue()
206
  plt.close(fig)
207
  buf.close()
208
-
209
- # Create new image from the raw bytes
210
  img = Image.open(BytesIO(img_data))
211
  return img
212
 
 
2
  import numpy as np
3
  from matplotlib import pyplot as plt
4
  from matplotlib.collections import LineCollection
 
5
  from PIL import Image
6
  from io import BytesIO
7
  from typing import Union
8
  import json
9
+ from fastf1.core import Session
10
 
11
  # Custom types
12
  gp = Union[str, int]
 
19
  return np.matmul(xy, rot_mat)
20
 
21
 
22
+ def create_track_speed_visualization(session: Session) -> Image:
 
23
 
24
  weekend = session.event
25
+ lap = session.laps.pick_fastest()
 
 
 
 
26
 
27
  # Get telemetry data
28
  x = lap.telemetry['X'] # values for x-axis
 
35
 
36
  # We create a plot with title and adjust some setting to make it look good.
37
  fig, ax = plt.subplots(sharex=True, sharey=True, figsize=(12, 6.75))
38
+ fig.suptitle(f'[Speed] {weekend["EventName"]} - {lap["Driver"]} #{lap["DriverNumber"]} ', size=24, y=0.97)
39
 
40
  # Adjust margins and turn of axis
41
  plt.subplots_adjust(left=0.1, right=0.9, top=0.9, bottom=0.12)
 
62
  normlegend = mpl.colors.Normalize(vmin=color.min(), vmax=color.max())
63
  legend = mpl.colorbar.ColorbarBase(cbaxes, norm=normlegend, cmap=mpl.colormaps['viridis'],
64
  orientation="horizontal")
65
+ legend.set_label("Speed [km/h]")
66
 
67
  # Create a PIL image from the plot
68
  fig = plt.gcf()
 
82
  return img
83
 
84
 
85
+ def create_track_corners_visualization(session: Session) -> Image:
86
 
87
  lap = session.laps.pick_fastest()
88
  pos = lap.get_pos_data()
 
158
  return img
159
 
160
 
161
+ def create_track_gear_visualization(session: Session) -> Image:
162
+ weekend = session.event
163
  lap = session.laps.pick_fastest()
164
  tel = lap.get_telemetry()
165
 
 
170
  segments = np.concatenate([points[:-1], points[1:]], axis=1)
171
  gear = tel['nGear'].to_numpy().astype(float)
172
 
173
+ fig, ax = plt.subplots(sharex=True, sharey=True, figsize=(12, 6.75))
174
+ fig.suptitle(f'[Gear] {weekend["EventName"]} - {lap["Driver"]} #{lap["DriverNumber"]}', size=24, x=0.5, ha='center', y=0.97)
175
+
176
+ plt.subplots_adjust(left=0.1, right=0.9, top=0.9, bottom=0.12)
177
+ ax.axis('off')
178
+
179
+ # Plot a background track line (black, thick) for context
180
+ ax.plot(lap.telemetry['X'], lap.telemetry['Y'],
181
+ color='black', linestyle='-', linewidth=16, zorder=0)
182
+
183
+ # Draw the colored segments
184
+ lc_comp = LineCollection(segments, norm=plt.Normalize(gear.min(), gear.max()), cmap=plt.cm.get_cmap('viridis', 8))
185
  lc_comp.set_array(gear)
186
  lc_comp.set_linewidth(4)
187
+ ax.add_collection(lc_comp)
188
 
189
+ # Set axis limits to the data range with padding to avoid clipping
190
+ x_pad = (x.max() - x.min()) * 0.03
191
+ y_pad = (y.max() - y.min()) * 0.03
192
+ ax.set_xlim(x.min() - x_pad, x.max() + x_pad)
193
+ ax.set_ylim(y.min() - y_pad, y.max() + y_pad)
194
 
195
+ # Set axis equal for correct aspect
196
+ ax.set_aspect('equal', adjustable='datalim')
 
 
197
 
198
+ # Add colorbar at the bottom
199
+ cbaxes = fig.add_axes([0.25, 0.05, 0.5, 0.05])
200
+ normlegend = plt.Normalize(1, 8)
201
+ legend = mpl.colorbar.ColorbarBase(cbaxes, norm=normlegend, cmap=plt.cm.get_cmap('viridis', 8),
202
+ orientation="horizontal")
203
+ legend.set_ticks(np.arange(1, 9))
204
+ legend.set_ticklabels(np.arange(1, 9))
205
+ legend.set_label("Gear")
206
 
207
  # Create a PIL image from the plot
 
 
 
208
  buf = BytesIO()
209
  fig.savefig(buf, format='png', dpi=150, bbox_inches='tight')
210
  buf.seek(0)
 
 
211
  img_data = buf.getvalue()
212
  plt.close(fig)
213
  buf.close()
 
 
214
  img = Image.open(BytesIO(img_data))
215
  return img
216