Buckets:
| 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 [ | |
| 'Emilia Romagna Grand Prix', | |
| 'French 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:
- 9549369b9a4cbc72482c4ae17c8136c315c60b70ffcbc8b378570247f8167873
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.