tracinginsights's picture
download
raw
23.6 kB
import json
import logging
import os
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
from typing import Any, Dict, List, Optional, Tuple, Union
import fastf1
import numpy as np
import pandas as pd
import requests
import utils
# Configure logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
handlers=[logging.FileHandler("telemetry_extraction.log"), logging.StreamHandler()],
)
logger = logging.getLogger("telemetry_extractor")
logging.getLogger("fastf1").setLevel(logging.WARNING)
logging.getLogger("fastf1").propagate = False
# Enable caching
fastf1.Cache.enable_cache("cache")
DEFAULT_YEAR = 2021
PROTO = "https"
HOST = "api.multiviewer.app"
HEADERS = {"User-Agent": f"FastF1/"}
# Global cache for session objects to prevent reloading
SESSION_CACHE = {}
CIRCUIT_INFO_CACHE = {}
class TelemetryExtractor:
"""Optimized class to handle extraction of F1 telemetry data."""
def __init__(
self,
year: int = DEFAULT_YEAR,
events: List[str] = None,
sessions: List[str] = None,
):
"""Initialize the TelemetryExtractor."""
self.year = year
self.events = events or [
'Hungarian Grand Prix',
'Mexico City Grand Prix',
]
self.sessions = sessions or ["Practice 1" , "Practice 2", "Practice 3", "Qualifying" ,"Race"]
def get_session(
self, event: Union[str, int], session: str, load_telemetry: bool = False
) -> fastf1.core.Session:
"""Get a cached session object to prevent reloading."""
cache_key = f"{self.year}-{event}-{session}"
if cache_key not in SESSION_CACHE:
f1session = fastf1.get_session(self.year, event, session)
f1session.load(telemetry=load_telemetry, weather=False, messages=False)
SESSION_CACHE[cache_key] = f1session
return SESSION_CACHE[cache_key]
def session_drivers_list(self, event: Union[str, int], session: str) -> List[str]:
"""Get list of driver codes for a given event and session."""
try:
f1session = self.get_session(event, session)
return list(f1session.laps["Driver"].unique())
except Exception as e:
logger.error(f"Error getting driver list for {event} {session}: {str(e)}")
return []
def session_drivers(
self, event: Union[str, int], session: str
) -> Dict[str, List[Dict[str, str]]]:
"""Get drivers available for a given event and session."""
try:
f1session = self.get_session(event, session)
laps = f1session.laps
team_colors = utils.team_colors(self.year)
laps["color"] = laps["Team"].map(team_colors)
unique_drivers = laps["Driver"].unique()
drivers = [
{
"driver": driver,
"team": laps[laps.Driver == driver].Team.iloc[0],
}
for driver in unique_drivers
]
return {"drivers": drivers}
except Exception as e:
logger.error(f"Error getting drivers for {event} {session}: {str(e)}")
return {"drivers": []}
def laps_data(
self, event: Union[str, int], session: str, driver: str, f1session=None
) -> Dict[str, List]:
"""Get lap data for a specific driver in a session."""
try:
if f1session is None:
f1session = self.get_session(event, session)
laps = f1session.laps
driver_laps = laps.pick_drivers(driver).copy() # Create a copy here
# Convert lap times to seconds and handle NaN values
lap_times = []
for lap_time in driver_laps["LapTime"]:
if hasattr(lap_time, "total_seconds"):
lap_times.append(lap_time.total_seconds())
elif pd.isna(lap_time): # Check if it's NaN
lap_times.append(None) # Use None instead of NaN
else:
lap_times.append(None)
# Handle NaN values in compounds
compounds = []
for compound in driver_laps["Compound"]:
if pd.isna(compound):
compounds.append(None) # Use None instead of NaN
else:
compounds.append(compound)
return {
"time": lap_times,
"lap": driver_laps["LapNumber"].tolist(),
"compound": compounds,
}
except Exception as e:
logger.error(
f"Error getting lap data for {driver} in {event} {session}: {str(e)}"
)
return {"time": [], "lap": [], "compound": []}
def accCalc(
self, telemetry: pd.DataFrame, Nax: int, Nay: int, Naz: int
) -> pd.DataFrame:
"""Calculate acceleration components from telemetry data."""
# Convert speed from km/h to m/s
vx = telemetry["Speed"] / 3.6
time_float = telemetry["Time"] / np.timedelta64(1, "s")
dtime = np.gradient(time_float)
ax = np.gradient(vx) / dtime
# Clean up outliers
for i in np.arange(1, len(ax) - 1).astype(int):
if ax[i] > 25:
ax[i] = ax[i - 1]
# Smooth x-acceleration
ax_smooth = np.convolve(ax, np.ones((Nax,)) / Nax, mode="same")
# Get position data
x = telemetry["X"]
y = telemetry["Y"]
z = telemetry["Z"]
# Calculate gradients
dx = np.gradient(x)
dy = np.gradient(y)
dz = np.gradient(z)
# Calculate theta (angle in xy-plane)
theta = np.arctan2(dy, (dx + np.finfo(float).eps))
theta[0] = theta[1]
theta_noDiscont = np.unwrap(theta)
# Calculate distance and curvature
dist = telemetry["Distance"]
ds = np.gradient(dist)
dtheta = np.gradient(theta_noDiscont)
# Clean up outliers
for i in np.arange(1, len(dtheta) - 1).astype(int):
if abs(dtheta[i]) > 0.5:
dtheta[i] = dtheta[i - 1]
# Calculate curvature and lateral acceleration
C = dtheta / (ds + 0.0001) # To avoid division by 0
ay = np.square(vx) * C
# Remove extreme values
indexProblems = np.abs(ay) > 150
ay[indexProblems] = 0
# Smooth y-acceleration
ay_smooth = np.convolve(ay, np.ones((Nay,)) / Nay, mode="same")
# Calculate z-acceleration (similar process)
z_theta = np.arctan2(dz, (dx + np.finfo(float).eps))
z_theta[0] = z_theta[1]
z_theta_noDiscont = np.unwrap(z_theta)
z_dtheta = np.gradient(z_theta_noDiscont)
# Clean up outliers
for i in np.arange(1, len(z_dtheta) - 1).astype(int):
if abs(z_dtheta[i]) > 0.5:
z_dtheta[i] = z_dtheta[i - 1]
# Calculate z-curvature and vertical acceleration
z_C = z_dtheta / (ds + 0.0001)
az = np.square(vx) * z_C
# Remove extreme values
indexProblems = np.abs(az) > 150
az[indexProblems] = 0
# Smooth z-acceleration
az_smooth = np.convolve(az, np.ones((Naz,)) / Naz, mode="same")
# Add acceleration columns to telemetry
telemetry["Ax"] = ax_smooth
telemetry["Ay"] = ay_smooth
telemetry["Az"] = az_smooth
return telemetry
def process_lap(
self,
event: str,
session: str,
driver: str,
lap_number: int,
driver_dir: str,
f1session=None,
driver_laps=None,
) -> bool:
"""Process a single lap for a driver."""
file_path = f"{driver_dir}/{lap_number}_tel.json"
# Skip if file already exists
if os.path.exists(file_path):
return True
try:
if f1session is None:
f1session = self.get_session(event, session, load_telemetry=True)
if driver_laps is None:
laps = f1session.laps
driver_laps = laps.pick_drivers(driver)
driver_laps["LapTime"] = driver_laps["LapTime"].apply(
lambda x: x.total_seconds() if hasattr(x, "total_seconds") else x
)
# Get the telemetry for lap_number
selected_lap = driver_laps[driver_laps.LapNumber == lap_number]
if selected_lap.empty:
logger.warning(
f"No data for {driver} lap {lap_number} in {event} {session}"
)
return False
telemetry = selected_lap.get_telemetry()
acc_tel = self.accCalc(telemetry, 3, 9, 9)
acc_tel["Time"] = acc_tel["Time"].dt.total_seconds()
# Create a unique data key for this telemetry
data_key = f"{self.year}-{event}-{session}-{driver}-{lap_number}"
# Convert DRS and Brake to binary values
acc_tel["DRS"] = acc_tel["DRS"].apply(
lambda x: 1 if x in [10, 12, 14] else 0
)
acc_tel["Brake"] = acc_tel["Brake"].apply(lambda x: 1 if x == True else 0)
telemetry_data = {
"tel": {
"time": acc_tel["Time"].tolist(),
"rpm": acc_tel["RPM"].tolist(),
"speed": acc_tel["Speed"].tolist(),
"gear": acc_tel["nGear"].tolist(),
"throttle": acc_tel["Throttle"].tolist(),
"brake": acc_tel["Brake"].tolist(),
"drs": acc_tel["DRS"].tolist(),
"distance": acc_tel["Distance"].tolist(),
"rel_distance": acc_tel["RelativeDistance"].tolist(),
"acc_x": acc_tel["Ax"].tolist(),
"acc_y": acc_tel["Ay"].tolist(),
"acc_z": acc_tel["Az"].tolist(),
"x": acc_tel["X"].tolist(),
"y": acc_tel["Y"].tolist(),
"z": acc_tel["Z"].tolist(),
"dataKey": data_key,
}
}
with open(file_path, "w") as json_file:
json.dump(telemetry_data, json_file)
return True
except Exception as e:
logger.error(f"Error processing lap {lap_number} for {driver}: {str(e)}")
return False
def get_circuit_info(self, event: str, session: str) -> Optional[Dict[str, List]]:
"""Get circuit corner information."""
cache_key = f"{self.year}-{event}-{session}"
if cache_key in CIRCUIT_INFO_CACHE:
return CIRCUIT_INFO_CACHE[cache_key]
try:
f1session = self.get_session(event, session)
circuit_key = f1session.session_info["Meeting"]["Circuit"]["Key"]
# Try to get corner data from fastf1 first
try:
circuit_info = f1session.get_circuit_info().corners
corner_info = {
"CornerNumber": circuit_info["Number"].tolist(),
"X": circuit_info["X"].tolist(),
"Y": circuit_info["Y"].tolist(),
"Angle": circuit_info["Angle"].tolist(),
"Distance": circuit_info["Distance"].tolist(),
}
CIRCUIT_INFO_CACHE[cache_key] = corner_info
return corner_info
except (AttributeError, KeyError):
# Fall back to API method if fastf1 method fails
circuit_info = self._get_circuit_info_from_api(circuit_key)
if circuit_info is not None:
corner_info = {
"CornerNumber": circuit_info["Number"].tolist(),
"X": circuit_info["X"].tolist(),
"Y": circuit_info["Y"].tolist(),
"Angle": circuit_info["Angle"].tolist(),
"Distance": (circuit_info["Distance"] / 10).tolist(),
}
CIRCUIT_INFO_CACHE[cache_key] = corner_info
return corner_info
logger.warning(f"Could not get corner data for {event} {session}")
return None
except Exception as e:
logger.error(f"Error getting circuit info for {event} {session}: {str(e)}")
return None
def _get_circuit_info_from_api(self, circuit_key: int) -> Optional[pd.DataFrame]:
"""Get circuit information from the MultiViewer API."""
url = f"{PROTO}://{HOST}/api/v1/circuits/{circuit_key}/{self.year}"
try:
response = requests.get(url, headers=HEADERS)
if response.status_code != 200:
logger.debug(f"[{response.status_code}] {response.content.decode()}")
return None
data = response.json()
rows = []
for entry in data["corners"]:
rows.append(
(
float(entry.get("trackPosition", {}).get("x", 0.0)),
float(entry.get("trackPosition", {}).get("y", 0.0)),
int(entry.get("number", 0)),
str(entry.get("letter", "")),
float(entry.get("angle", 0.0)),
float(entry.get("length", 0.0)),
)
)
return pd.DataFrame(
rows, columns=["X", "Y", "Number", "Letter", "Angle", "Distance"]
)
except Exception as e:
logger.error(f"Error fetching circuit data from API: {str(e)}")
return None
def process_driver(
self, event: str, session: str, driver: str, base_dir: str, f1session=None
) -> None:
"""Process all laps for a single driver."""
driver_dir = f"{base_dir}/{driver}"
os.makedirs(driver_dir, exist_ok=True)
try:
if f1session is None:
f1session = self.get_session(event, session, load_telemetry=True)
# Save lap times
laptimes = self.laps_data(event, session, driver, f1session)
# Replace NaN values with None before JSON serialization
laptimes["time"] = ["None" if pd.isna(x) else x for x in laptimes["time"]]
laptimes["lap"] = ["None" if pd.isna(x) else x for x in laptimes["lap"]]
laptimes["compound"] = ["None" if pd.isna(x) else x for x in laptimes["compound"]]
with open(f"{driver_dir}/laptimes.json", "w") as json_file:
json.dump(laptimes, json_file)
# Get driver laps
laps = f1session.laps
driver_laps = laps.pick_drivers(driver).copy() # Create a copy here
driver_laps["LapNumber"] = driver_laps["LapNumber"].astype(int)
driver_laps["LapTime"] = driver_laps["LapTime"].apply(
lambda x: x.total_seconds() if hasattr(x, "total_seconds") else x
)
lap_numbers = driver_laps["LapNumber"].tolist()
# Process laps in parallel
with ThreadPoolExecutor(max_workers=4) as executor:
futures = [
executor.submit(
self.process_lap,
event,
session,
driver,
lap_number,
driver_dir,
f1session,
driver_laps,
)
for lap_number in lap_numbers
]
for future in as_completed(futures):
future.result() # Just to catch any exceptions
except Exception as e:
logger.error(f"Error processing driver {driver}: {str(e)}")
def process_event_session(self, event: str, session: str) -> None:
"""Process a single event and session, extracting all telemetry data."""
logger.info(f"Processing {event} - {session}")
# Create base directory for this event/session
base_dir = f"{event}/{session}"
os.makedirs(base_dir, exist_ok=True)
try:
# Load session data once
f1session = self.get_session(event, session, load_telemetry=True)
# Save drivers information
drivers_info = self.session_drivers(event, session)
with open(f"{base_dir}/drivers.json", "w") as json_file:
json.dump(drivers_info, json_file)
# Save circuit corner information
corner_info = self.get_circuit_info(event, session)
if corner_info:
with open(f"{base_dir}/corners.json", "w") as json_file:
json.dump(corner_info, json_file)
# Get driver list
drivers = self.session_drivers_list(event, session)
# Process drivers in parallel
with ThreadPoolExecutor(max_workers=8) as executor:
futures = [
executor.submit(
self.process_driver, event, session, driver, base_dir, f1session
)
for driver in drivers
]
for future in as_completed(futures):
future.result() # Just to catch any exceptions
except Exception as e:
logger.error(f"Error processing {event} - {session}: {str(e)}")
def process_all_data(self, max_workers: int = 4) -> None:
"""Process all configured events and sessions, with parallelization."""
logger.info(f"Starting optimized telemetry extraction for {self.year} season")
logger.info(f"Events: {self.events}")
logger.info(f"Sessions: {self.sessions}")
start_time = time.time()
# Process each event and session in parallel
with ThreadPoolExecutor(max_workers=max_workers) as executor:
futures = []
for event in self.events:
for session in self.sessions:
futures.append(
executor.submit(self.process_event_session, event, session)
)
# Wait for all tasks to complete
for future in as_completed(futures):
try:
future.result()
except Exception as e:
logger.error(f"Error in processing task: {str(e)}")
elapsed_time = time.time() - start_time
logger.info(f"Telemetry extraction completed in {elapsed_time:.2f} seconds")
import gc
import logging
import os
import psutil
logger = logging.getLogger("memory_monitor")
def check_memory_usage(threshold_percent=80):
"""
Check if memory usage exceeds threshold and clear caches if needed.
Args:
threshold_percent: Memory usage percentage threshold
Returns:
True if memory was cleared, False otherwise
"""
process = psutil.Process(os.getpid())
memory_info = process.memory_info()
memory_percent = process.memory_percent()
logger.info(
f"Current memory usage: {memory_percent:.2f}% ({memory_info.rss / 1024 / 1024:.2f} MB)"
)
if memory_percent > threshold_percent:
logger.warning(
f"Memory usage exceeds {threshold_percent}% threshold, clearing caches"
)
# Clear the session cache
SESSION_CACHE.clear()
CIRCUIT_INFO_CACHE.clear()
# Force garbage collection
gc.collect()
# Log new memory usage
new_memory_percent = psutil.Process(os.getpid()).memory_percent()
logger.info(
f"New memory usage after clearing caches: {new_memory_percent:.2f}%"
)
return True
return False
def is_data_available(year, events, sessions):
"""
Check if data is available for the specified year, events, and sessions.
Args:
year: The F1 season year
events: List of event names to check
sessions: List of session names to check
Returns:
bool: True if data is available, False otherwise
"""
try:
# Try to load the first event and session as a test
if not events or not sessions:
logger.warning("No events or sessions specified to check")
return False
event = events[0]
session = sessions[0]
logger.info(f"Checking data availability for {year} {event} {session}...")
# Try to get the session without loading telemetry
f1session = fastf1.get_session(year, event, session)
f1session.load(telemetry=False, weather=False, messages=False)
# Check if we have lap data
if f1session.laps.empty:
logger.info(f"No lap data available yet for {year} {event} {session}")
return False
# Check if we have at least one driver
if len(f1session.laps["Driver"].unique()) == 0:
logger.info(f"No driver data available yet for {year} {event} {session}")
return False
logger.info(f"Data is available for {year} {event} {session}")
return True
except Exception as e:
logger.info(f"Data not yet available: {str(e)}")
return False
def main():
"""Main entry point for the script."""
try:
# Create extractor
extractor = TelemetryExtractor()
# Use more workers on GitHub Actions
is_github_actions = os.environ.get("GITHUB_ACTIONS") == "true"
max_workers = 12 if is_github_actions else 8
# Wait for data to be available
wait_time = 30 # seconds between checks
max_attempts = 720 # 12 hours max wait time (720 * 60 seconds)
attempt = 0
logger.info(f"Starting to wait for {extractor.year} season data...")
while attempt < max_attempts:
if is_data_available(extractor.year, extractor.events, extractor.sessions):
logger.info(
f"Data is available for {extractor.year} season. Starting extraction..."
)
extractor.process_all_data(max_workers=max_workers)
break
else:
attempt += 1
logger.info(
f"Data not yet available. Waiting {wait_time} seconds before retry ({attempt}/{max_attempts})..."
)
time.sleep(wait_time)
# Check memory usage and clear if needed
check_memory_usage()
if attempt >= max_attempts:
logger.error(
f"Exceeded maximum wait time ({max_attempts * wait_time / 3600} hours). Exiting."
)
except Exception as e:
logger.error(f"Error in main function: {str(e)}")
raise
if __name__ == "__main__":
main()

Xet Storage Details

Size:
23.6 kB
·
Xet hash:
c8ba2c965084d6a3e314655f15fa135fec01b0793370fb380f8b10469a493c0e

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.