Skip to content

Commit

Permalink
ENH: add track markers from MV API and expose session info
Browse files Browse the repository at this point in the history
  • Loading branch information
theOehrly committed Aug 21, 2023
1 parent 44cfd90 commit 5927d0d
Show file tree
Hide file tree
Showing 15 changed files with 470 additions and 2 deletions.
8 changes: 8 additions & 0 deletions docs/changelog/v3.1.x.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ What's new in v3.1.0
New Features
^^^^^^^^^^^^

- Added additional circuit information that includes the location of corners,
marshal lights, marshal sectors and the rotation of the track map. This
data is available through :func:`fastf1.core.Session.get_circuit_info`. Also
check out the new examples that show how to annotate your plots using this
data.
A huge thank you to @multiviewer (https://multiviewer.app/) for providing
this information through MultiViewer's API.

- "SessionInfo" data from the F1 livetiming API is now available through
:attr:`fastf1.core.Session.session_info`

Expand Down
8 changes: 8 additions & 0 deletions docs/circuit_info.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.. _circuit_info:

Circuit Information
===================

.. autoclass:: fastf1.mvapi.CircuitInfo
:members:

7 changes: 7 additions & 0 deletions docs/core.rst
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,13 @@
:show-inheritance:


CircuitInfo
+++++++++++

.. autoclass:: fastf1.mvapi.data.CircuitInfo
:members:
:autosummary:


Exceptions
**********
Expand Down
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ Contents
events
api
ergast
circuit_info
utils
plotting
livetiming
Expand Down
112 changes: 112 additions & 0 deletions examples/plot_annotate_corners.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
"""Draw a track map with numbered corners
=========================================
Use the position data of a single lap to draw a track map.
Then annotate the map with corner numbers.
"""
##############################################################################
# Import FastF1 and load the data. Use the telemetry from the fastest for the
# track map. (You could also use any other lap instead.)

import fastf1

import matplotlib.pyplot as plt
import numpy as np


session = fastf1.get_session(2023, 'Silverstone', 'Q')
session.load()

lap = session.laps.pick_fastest()
pos = lap.get_pos_data()

circuit_info = session.get_circuit_info()


##############################################################################
# Define a helper function for rotating points around the origin of the
# coordinate system.
#
# The matrix ``[[cos, sin], [-sin, cos]]`` is called a rotation matrix.
#
# By matrix multiplication of the rotation matrix with a vector [x, y], a new
# rotated vector [x_rot, y_rot] is obtained.
# (See also: https://en.wikipedia.org/wiki/Rotation_matrix)

def rotate(xy, *, angle):
rot_mat = np.array([[np.cos(angle), np.sin(angle)],
[-np.sin(angle), np.cos(angle)]])
return np.matmul(xy, rot_mat)


##############################################################################
# Get the coordinates of the track map from the telemetry of the lap and
# rotate the coordinates using the rotation from ``circuit_info`` so that
# the track map is oriented correctly. After that, plot the rotated track map.

# Get an array of shape [n, 2] where n is the number of points and the second
# axis is x and y.
track = pos.loc[:, ('X', 'Y')].to_numpy()

# Convert the rotation angle from degrees to radian.
track_angle = circuit_info.rotation / 180 * np.pi

# Rotate and plot the track map.
rotated_track = rotate(track, angle=track_angle)
plt.plot(rotated_track[:, 0], rotated_track[:, 1])

# sphinx_gallery_defer_figures


##############################################################################
# Finally, the corner markers are plotted. To plot the numbers next to the
# track, an offset vector that points straight up is defined. This offset
# vector is then rotated by the angle that is given for each corner marker.
# A line and circular bubble are drawn and the corner marker text is printed
# inside the bubble.

offset_vector = [500, 0] # offset length is chosen arbitrarily to 'look good'

# Iterate over all corners.
for _, corner in circuit_info.corners.iterrows():
# Create a string from corner number and letter
txt = f"{corner['Number']}{corner['Letter']}"

# Convert the angle from degrees to radian.
offset_angle = corner['Angle'] / 180 * np.pi

# Rotate the offset vector so that it points sideways from the track.
offset_x, offset_y = rotate(offset_vector, angle=offset_angle)

# Add the offset to the position of the corner
text_x = corner['X'] + offset_x
text_y = corner['Y'] + offset_y

# Rotate the text position equivalently to the rest of the track map
text_x, text_y = rotate([text_x, text_y], angle=track_angle)

# Rotate the center of the corner equivalently to the rest of the track map
track_x, track_y = rotate([corner['X'], corner['Y']], angle=track_angle)

# Draw a circle next to the track.
plt.scatter(text_x, text_y, color='grey', s=140)

# Draw a line from the track to this circle.
plt.plot([track_x, text_x], [track_y, text_y], color='grey')

# Finally, print the corner number inside the circle.
plt.text(text_x, text_y, txt,
va='center_baseline', ha='center', size='small', color='white')

# sphinx_gallery_defer_figures


##############################################################################
# Add a title, remove tick labels to clean up the plot, set equal axis ratio,
# so that the track is not distorted and show the plot.

plt.title(session.event['Location'])
plt.xticks([])
plt.yticks([])
plt.axis('equal')
plt.show()
67 changes: 67 additions & 0 deletions examples/plot_annotate_speed_trace.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
"""Plot speed traces with corner annotations
============================================
Plot the speed over the course of a lap and add annotations to mark corners.
"""


import matplotlib.pyplot as plt

import fastf1.plotting


# enable some matplotlib patches for plotting timedelta values and load
# FastF1's default color scheme
fastf1.plotting.setup_mpl(misc_mpl_mods=False)

# load a session and its telemetry data
session = fastf1.get_session(2021, 'Spanish Grand Prix', 'Q')
session.load()

##############################################################################
# First, we select the fastest lap and get the car telemetry data for this
# lap.

fastest_lap = session.laps.pick_fastest()
car_data = fastest_lap.get_car_data().add_distance()

##############################################################################
# Next, load the circuit info that includes the information about the location
# of the corners.

circuit_info = session.get_circuit_info()

##############################################################################
# Finally, we create a plot and plot the speed trace as well as the corner
# markers.

team_color = fastf1.plotting.team_color(fastest_lap['Team'])

fig, ax = plt.subplots()
ax.plot(car_data['Distance'], car_data['Speed'],
color=team_color, label=fastest_lap['Driver'])

# Draw vertical dotted lines at each corner that range from slightly below the
# minimum speed to slightly above the maximum speed.
v_min = car_data['Speed'].min()
v_max = car_data['Speed'].max()
ax.vlines(x=circuit_info.corners['Distance'], ymin=v_min-20, ymax=v_max+20,
linestyles='dotted', colors='grey')

# Plot the corner number just below each vertical line.
# For corners that are very close together, the text may overlap. A more
# complicated approach would be necessary to reliably prevent this.
for _, corner in circuit_info.corners.iterrows():
txt = f"{corner['Number']}{corner['Letter']}"
ax.text(corner['Distance'], v_min-30, txt,
va='center_baseline', ha='center', size='small')

ax.set_xlabel('Distance in m')
ax.set_ylabel('Speed in km/h')
ax.legend()

# Manually adjust the y-axis limits to include the corner numbers, because
# Matplotlib does not automatically account for text that was manually added.
ax.set_ylim([v_min - 40, v_max + 20])

plt.show()
20 changes: 20 additions & 0 deletions fastf1/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
import fastf1
from fastf1 import _api as api
from fastf1 import ergast
from fastf1.mvapi import get_circuit_info, CircuitInfo
from fastf1.logger import get_logger, soft_exceptions
from fastf1.utils import to_timedelta

Expand Down Expand Up @@ -2157,6 +2158,25 @@ def get_driver(self, identifier) -> "DriverResult":
raise ValueError(f"Invalid driver identifier '{identifier}'")
return self.results[mask].iloc[0]

def get_circuit_info(self) -> Optional[CircuitInfo]:
"""Returns additional information about the circuit that hosts this
event.
This information includes the location of corners, marshal lights,
marshal sectors and the rotation of the track map. Note that the data
is manually created and therefore not highly accurate, but it is useful
for annotating data visualizations.
See :class:`~fastf1.mvapi.CircuitInfo` for detailed information.
"""
circuit_key = self.session_info['Meeting']['Circuit']['Key']
circuit_info = get_circuit_info(year=self.event.year,
circuit_key=circuit_key)
circuit_info.add_marker_distance(
reference_lap=self.laps.pick_fastest()
)
return circuit_info

def _calculate_t0_date(self, car_data, pos_data):
"""Calculate the date timestamp at which data for this session is starting.
Expand Down
1 change: 1 addition & 0 deletions fastf1/mvapi/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from fastf1.mvapi.data import get_circuit_info, CircuitInfo # noqa F401
32 changes: 32 additions & 0 deletions fastf1/mvapi/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import requests.exceptions

from typing import Optional

from fastf1.mvapi.internals import _logger
from fastf1.req import Cache
from fastf1.version import __version__


PROTO = "https"
HOST = "api.multiviewer.app"
HEADERS = {'User-Agent': f'FastF1/{__version__}'}


def _make_url(path: str):
return f"{PROTO}://{HOST}{path}"


def get_circuit(*, year: int, circuit_key: int) -> Optional[dict]:
""":meta private:
Request circuit data from the MultiViewer API and return the JSON
response."""
url = _make_url(f"/api/v1/circuits/{circuit_key}/{year}")
response = Cache.requests_get(url, headers=HEADERS)
if response.status_code != 200:
_logger.debug(f"[{response.status_code}] {response.content.decode()}")
return None

try:
return response.json()
except requests.exceptions.JSONDecodeError:
return None
Loading

0 comments on commit 5927d0d

Please sign in to comment.