Files
Guido.Tech/hpcsim/enrichment.py

256 lines
9.4 KiB
Python
Raw Normal View History

2025-10-18 19:15:41 -05:00
from __future__ import annotations
from dataclasses import dataclass, field
2025-10-19 02:00:56 -05:00
from typing import Dict, Any, Optional, List
import pandas as pd
2025-10-18 19:15:41 -05:00
# --- LAP-LEVEL TELEMETRY CONTRACT ---
# Input from Raspberry Pi (lap-level data):
2025-10-18 19:15:41 -05:00
# {
# "lap_number": 27,
# "total_laps": 51,
# "lap_time": "0 days 00:01:27.318000",
# "average_speed": 234.62,
# "max_speed": 333.0,
# "tire_compound": "MEDIUM",
# "tire_life_laps": 19,
# "track_temperature": 43.6,
# "rainfall": false
2025-10-18 19:15:41 -05:00
# }
_TIRE_DEGRADATION_RATES = {
"soft": 0.030, # Fast degradation
"medium": 0.020, # Moderate degradation
"hard": 0.015, # Slow degradation
"inter": 0.025,
"wet": 0.022,
2025-10-18 19:15:41 -05:00
}
_TIRE_CLIFF_THRESHOLD = 25 # Laps before cliff risk increases significantly
2025-10-18 19:15:41 -05:00
@dataclass
class EnricherState:
"""Maintains race state across laps for trend analysis."""
lap_times: List[float] = field(default_factory=list) # Recent lap times in seconds
lap_speeds: List[float] = field(default_factory=list) # Recent average speeds
current_tire_age: int = 0
current_tire_compound: str = "medium"
tire_stint_start_lap: int = 1
total_laps: int = 51
track_name: str = "Monza"
2025-10-18 19:15:41 -05:00
class Enricher:
"""
HPC-simulated enrichment for lap-level F1 telemetry.
Accepts lap-level data from Raspberry Pi and generates performance insights
that simulate HPC computational analysis.
2025-10-18 19:15:41 -05:00
"""
def __init__(self):
self.state = EnricherState()
self._baseline_lap_time: Optional[float] = None # Best lap time as baseline
def enrich_lap_data(self, lap_data: Dict[str, Any]) -> Dict[str, Any]:
"""
Main enrichment method for lap-level data.
Returns enriched telemetry + race context for AI layer.
"""
# Extract lap data
lap_number = int(lap_data.get("lap_number", 0))
total_laps = int(lap_data.get("total_laps", 51))
lap_time_str = lap_data.get("lap_time")
average_speed = float(lap_data.get("average_speed", 0.0))
max_speed = float(lap_data.get("max_speed", 0.0))
tire_compound = str(lap_data.get("tire_compound", "MEDIUM")).lower()
tire_life_laps = int(lap_data.get("tire_life_laps", 0))
track_temperature = float(lap_data.get("track_temperature", 25.0))
rainfall = bool(lap_data.get("rainfall", False))
2025-10-19 02:00:56 -05:00
# Convert lap time to seconds
lap_time_seconds = self._parse_lap_time(lap_time_str)
# Update state
self.state.lap_times.append(lap_time_seconds)
self.state.lap_speeds.append(average_speed)
self.state.current_tire_age = tire_life_laps
self.state.current_tire_compound = tire_compound
self.state.total_laps = total_laps
# Keep only last 10 laps for analysis
if len(self.state.lap_times) > 10:
self.state.lap_times = self.state.lap_times[-10:]
self.state.lap_speeds = self.state.lap_speeds[-10:]
# Set baseline (best lap time)
if self._baseline_lap_time is None or lap_time_seconds < self._baseline_lap_time:
self._baseline_lap_time = lap_time_seconds
# Compute HPC-simulated insights
tire_deg_rate = self._compute_tire_degradation_rate(tire_compound, tire_life_laps, track_temperature)
pace_trend = self._compute_pace_trend()
tire_cliff_risk = self._compute_tire_cliff_risk(tire_compound, tire_life_laps)
pit_window = self._compute_optimal_pit_window(lap_number, total_laps, tire_life_laps, tire_compound)
performance_delta = self._compute_performance_delta(lap_time_seconds)
2025-10-19 02:00:56 -05:00
# Build enriched telemetry
enriched_telemetry = {
"lap": lap_number,
"tire_degradation_rate": round(tire_deg_rate, 3),
"pace_trend": pace_trend,
"tire_cliff_risk": round(tire_cliff_risk, 3),
"optimal_pit_window": pit_window,
"performance_delta": round(performance_delta, 2)
2025-10-19 02:00:56 -05:00
}
# Build race context
race_context = {
"race_info": {
"track_name": self.state.track_name,
"total_laps": total_laps,
"current_lap": lap_number,
"weather_condition": "Wet" if rainfall else "Dry",
"track_temp_celsius": track_temperature
},
"driver_state": {
"driver_name": "Alonso",
"current_position": 5, # Mock - could be passed in
"current_tire_compound": tire_compound,
"tire_age_laps": tire_life_laps,
"fuel_remaining_percent": self._estimate_fuel(lap_number, total_laps)
}
}
2025-10-19 02:00:56 -05:00
return {
"enriched_telemetry": enriched_telemetry,
"race_context": race_context
}
# --- HPC-Simulated Computation Methods ---
def _compute_tire_degradation_rate(self, tire_compound: str, tire_age: int, track_temp: float) -> float:
"""
Simulate HPC computation of tire degradation rate.
Returns 0-1 value (higher = worse degradation).
"""
base_rate = _TIRE_DEGRADATION_RATES.get(tire_compound, 0.020)
2025-10-19 02:00:56 -05:00
# Temperature effect: higher temp = more degradation
temp_multiplier = 1.0
if track_temp > 45:
temp_multiplier = 1.3
elif track_temp > 40:
temp_multiplier = 1.15
elif track_temp < 20:
temp_multiplier = 0.9
2025-10-19 02:00:56 -05:00
# Age effect: exponential increase after certain threshold
age_multiplier = 1.0
if tire_age > 20:
age_multiplier = 1.0 + ((tire_age - 20) * 0.05) # +5% per lap over 20
2025-10-19 02:00:56 -05:00
degradation = base_rate * tire_age * temp_multiplier * age_multiplier
return min(1.0, degradation)
2025-10-19 02:00:56 -05:00
def _compute_pace_trend(self) -> str:
"""
Analyze recent lap times to determine pace trend.
Returns: "improving", "stable", or "declining"
"""
if len(self.state.lap_times) < 3:
return "stable"
2025-10-19 02:00:56 -05:00
recent_laps = self.state.lap_times[-5:] # Last 5 laps
2025-10-19 02:00:56 -05:00
# Calculate trend (simple linear regression)
avg_first_half = sum(recent_laps[:len(recent_laps)//2]) / max(1, len(recent_laps)//2)
avg_second_half = sum(recent_laps[len(recent_laps)//2:]) / max(1, len(recent_laps) - len(recent_laps)//2)
2025-10-19 02:00:56 -05:00
diff = avg_second_half - avg_first_half
2025-10-19 02:00:56 -05:00
if diff < -0.5: # Getting faster by more than 0.5s
return "improving"
elif diff > 0.5: # Getting slower by more than 0.5s
return "declining"
else:
return "stable"
def _compute_tire_cliff_risk(self, tire_compound: str, tire_age: int) -> float:
"""
Compute probability of hitting tire performance cliff.
Returns 0-1 (0 = no risk, 1 = imminent cliff).
"""
# Different compounds have different cliff points
cliff_points = {
"soft": 15,
"medium": 25,
"hard": 35,
"inter": 20,
"wet": 18
}
2025-10-19 02:00:56 -05:00
cliff_point = cliff_points.get(tire_compound, 25)
if tire_age < cliff_point - 5:
return 0.0
elif tire_age >= cliff_point + 5:
return 1.0
2025-10-18 19:15:41 -05:00
else:
# Linear risk increase in 10-lap window around cliff point
return (tire_age - (cliff_point - 5)) / 10.0
def _compute_optimal_pit_window(self, current_lap: int, total_laps: int, tire_age: int, tire_compound: str) -> List[int]:
"""
Calculate optimal pit stop window based on tire degradation.
Returns [start_lap, end_lap] for pit window.
"""
cliff_risk = self._compute_tire_cliff_risk(tire_compound, tire_age)
if cliff_risk > 0.7:
# Urgent pit needed
return [current_lap + 1, current_lap + 3]
elif cliff_risk > 0.4:
# Pit soon
return [current_lap + 3, current_lap + 6]
else:
# Tire still good, estimate based on compound
if tire_compound == "soft":
laps_remaining = max(0, 18 - tire_age)
elif tire_compound == "medium":
laps_remaining = max(0, 28 - tire_age)
else: # hard
laps_remaining = max(0, 38 - tire_age)
pit_lap = min(current_lap + laps_remaining, total_laps - 5)
return [max(current_lap + 1, pit_lap - 2), pit_lap + 2]
def _compute_performance_delta(self, current_lap_time: float) -> float:
"""
Calculate performance delta vs baseline lap time.
Negative = slower than baseline, Positive = faster.
"""
if self._baseline_lap_time is None:
return 0.0
return self._baseline_lap_time - current_lap_time # Negative if slower
def _estimate_fuel(self, current_lap: int, total_laps: int) -> float:
"""Estimate remaining fuel percentage based on lap progression."""
return max(0.0, 100.0 * (1.0 - (current_lap / total_laps)))
def _parse_lap_time(self, lap_time_str: Optional[str]) -> float:
"""Convert lap time string to seconds."""
if not lap_time_str:
return 90.0 # Default ~1:30
try:
# Handle pandas Timedelta string format
td = pd.to_timedelta(lap_time_str)
return td.total_seconds()
except:
return 90.0