p
This commit is contained in:
Binary file not shown.
Binary file not shown.
74
ai_intelligence_layer/utils/telemetry_buffer.py
Normal file
74
ai_intelligence_layer/utils/telemetry_buffer.py
Normal file
@@ -0,0 +1,74 @@
|
||||
"""
|
||||
In-memory buffer for storing enriched telemetry data received via webhooks.
|
||||
"""
|
||||
from collections import deque
|
||||
from typing import List, Optional
|
||||
import logging
|
||||
from models.input_models import EnrichedTelemetryWebhook
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TelemetryBuffer:
|
||||
"""In-memory buffer for enriched telemetry data."""
|
||||
|
||||
def __init__(self, max_size: int = 100):
|
||||
"""
|
||||
Initialize telemetry buffer.
|
||||
|
||||
Args:
|
||||
max_size: Maximum number of records to store
|
||||
"""
|
||||
self._buffer = deque(maxlen=max_size)
|
||||
self.max_size = max_size
|
||||
logger.info(f"Telemetry buffer initialized (max_size={max_size})")
|
||||
|
||||
def add(self, telemetry: EnrichedTelemetryWebhook):
|
||||
"""
|
||||
Add telemetry record to buffer.
|
||||
|
||||
Args:
|
||||
telemetry: Enriched telemetry data
|
||||
"""
|
||||
self._buffer.append(telemetry)
|
||||
logger.debug(f"Added telemetry for lap {telemetry.lap} (buffer size: {len(self._buffer)})")
|
||||
|
||||
def get_latest(self, limit: int = 10) -> List[EnrichedTelemetryWebhook]:
|
||||
"""
|
||||
Get latest telemetry records.
|
||||
|
||||
Args:
|
||||
limit: Maximum number of records to return
|
||||
|
||||
Returns:
|
||||
List of most recent telemetry records (newest first)
|
||||
"""
|
||||
# Get last N items, return in reverse order (newest first)
|
||||
items = list(self._buffer)[-limit:]
|
||||
items.reverse()
|
||||
return items
|
||||
|
||||
def get_all(self) -> List[EnrichedTelemetryWebhook]:
|
||||
"""
|
||||
Get all telemetry records in buffer.
|
||||
|
||||
Returns:
|
||||
List of all telemetry records (newest first)
|
||||
"""
|
||||
items = list(self._buffer)
|
||||
items.reverse()
|
||||
return items
|
||||
|
||||
def size(self) -> int:
|
||||
"""
|
||||
Get current buffer size.
|
||||
|
||||
Returns:
|
||||
Number of records in buffer
|
||||
"""
|
||||
return len(self._buffer)
|
||||
|
||||
def clear(self):
|
||||
"""Clear all records from buffer."""
|
||||
self._buffer.clear()
|
||||
logger.info("Telemetry buffer cleared")
|
||||
278
ai_intelligence_layer/utils/validators.py
Normal file
278
ai_intelligence_layer/utils/validators.py
Normal file
@@ -0,0 +1,278 @@
|
||||
"""
|
||||
Validators for strategy validation and telemetry analysis.
|
||||
"""
|
||||
from typing import List, Tuple
|
||||
import logging
|
||||
from models.input_models import Strategy, RaceContext, EnrichedTelemetryWebhook
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class StrategyValidator:
|
||||
"""Validates race strategies against F1 rules and constraints."""
|
||||
|
||||
@staticmethod
|
||||
def validate_strategy(strategy: Strategy, race_context: RaceContext) -> Tuple[bool, str]:
|
||||
"""
|
||||
Validate a single strategy.
|
||||
|
||||
Args:
|
||||
strategy: Strategy to validate
|
||||
race_context: Current race context
|
||||
|
||||
Returns:
|
||||
Tuple of (is_valid, error_message)
|
||||
"""
|
||||
current_lap = race_context.race_info.current_lap
|
||||
total_laps = race_context.race_info.total_laps
|
||||
|
||||
# Check pit laps are within valid range
|
||||
for pit_lap in strategy.pit_laps:
|
||||
if pit_lap <= current_lap:
|
||||
return False, f"Pit lap {pit_lap} is in the past (current lap: {current_lap})"
|
||||
if pit_lap >= total_laps:
|
||||
return False, f"Pit lap {pit_lap} is beyond race end (total laps: {total_laps})"
|
||||
|
||||
# Check pit laps are in order
|
||||
if len(strategy.pit_laps) > 1:
|
||||
if strategy.pit_laps != sorted(strategy.pit_laps):
|
||||
return False, "Pit laps must be in ascending order"
|
||||
|
||||
# Check stop count matches pit laps
|
||||
if len(strategy.pit_laps) != strategy.stop_count:
|
||||
return False, f"Stop count ({strategy.stop_count}) doesn't match pit laps ({len(strategy.pit_laps)})"
|
||||
|
||||
# Check tire sequence length
|
||||
expected_tire_count = strategy.stop_count + 1
|
||||
if len(strategy.tire_sequence) != expected_tire_count:
|
||||
return False, f"Tire sequence length ({len(strategy.tire_sequence)}) doesn't match stops + 1"
|
||||
|
||||
# Check at least 2 different compounds (F1 rule)
|
||||
unique_compounds = set(strategy.tire_sequence)
|
||||
if len(unique_compounds) < 2:
|
||||
return False, "Must use at least 2 different tire compounds (F1 rule)"
|
||||
|
||||
return True, ""
|
||||
|
||||
@staticmethod
|
||||
def validate_strategies(strategies: List[Strategy], race_context: RaceContext) -> List[Strategy]:
|
||||
"""
|
||||
Validate all strategies and filter out invalid ones.
|
||||
|
||||
Args:
|
||||
strategies: List of strategies to validate
|
||||
race_context: Current race context
|
||||
|
||||
Returns:
|
||||
List of valid strategies
|
||||
"""
|
||||
valid_strategies = []
|
||||
|
||||
for strategy in strategies:
|
||||
is_valid, error = StrategyValidator.validate_strategy(strategy, race_context)
|
||||
if is_valid:
|
||||
valid_strategies.append(strategy)
|
||||
else:
|
||||
logger.warning(f"Strategy {strategy.strategy_id} invalid: {error}")
|
||||
|
||||
logger.info(f"Validated {len(valid_strategies)}/{len(strategies)} strategies")
|
||||
return valid_strategies
|
||||
|
||||
|
||||
class TelemetryAnalyzer:
|
||||
"""Analyzes enriched telemetry data to extract trends and insights."""
|
||||
|
||||
@staticmethod
|
||||
def calculate_tire_degradation_rate(telemetry: List[EnrichedTelemetryWebhook]) -> float:
|
||||
"""
|
||||
Calculate tire degradation rate per lap.
|
||||
|
||||
Args:
|
||||
telemetry: List of enriched telemetry records
|
||||
|
||||
Returns:
|
||||
Rate of tire degradation per lap (0.0 to 1.0)
|
||||
"""
|
||||
if len(telemetry) < 2:
|
||||
return 0.0
|
||||
|
||||
# Sort by lap (ascending)
|
||||
sorted_telemetry = sorted(telemetry, key=lambda x: x.lap)
|
||||
|
||||
# Calculate rate of change
|
||||
first = sorted_telemetry[0]
|
||||
last = sorted_telemetry[-1]
|
||||
|
||||
lap_diff = last.lap - first.lap
|
||||
if lap_diff == 0:
|
||||
return 0.0
|
||||
|
||||
deg_diff = last.tire_degradation_index - first.tire_degradation_index
|
||||
rate = deg_diff / lap_diff
|
||||
|
||||
return max(0.0, rate) # Ensure non-negative
|
||||
|
||||
@staticmethod
|
||||
def calculate_aero_efficiency_avg(telemetry: List[EnrichedTelemetryWebhook]) -> float:
|
||||
"""
|
||||
Calculate average aero efficiency.
|
||||
|
||||
Args:
|
||||
telemetry: List of enriched telemetry records
|
||||
|
||||
Returns:
|
||||
Average aero efficiency (0.0 to 1.0)
|
||||
"""
|
||||
if not telemetry:
|
||||
return 0.0
|
||||
|
||||
total = sum(t.aero_efficiency for t in telemetry)
|
||||
return total / len(telemetry)
|
||||
|
||||
@staticmethod
|
||||
def analyze_ers_pattern(telemetry: List[EnrichedTelemetryWebhook]) -> str:
|
||||
"""
|
||||
Analyze ERS charge pattern.
|
||||
|
||||
Args:
|
||||
telemetry: List of enriched telemetry records
|
||||
|
||||
Returns:
|
||||
Pattern description: "charging", "stable", "depleting"
|
||||
"""
|
||||
if len(telemetry) < 2:
|
||||
return "stable"
|
||||
|
||||
# Sort by lap
|
||||
sorted_telemetry = sorted(telemetry, key=lambda x: x.lap)
|
||||
|
||||
# Look at recent trend
|
||||
recent = sorted_telemetry[-3:] if len(sorted_telemetry) >= 3 else sorted_telemetry
|
||||
|
||||
if len(recent) < 2:
|
||||
return "stable"
|
||||
|
||||
# Calculate average change
|
||||
total_change = 0.0
|
||||
for i in range(1, len(recent)):
|
||||
total_change += recent[i].ers_charge - recent[i-1].ers_charge
|
||||
|
||||
avg_change = total_change / (len(recent) - 1)
|
||||
|
||||
if avg_change > 0.05:
|
||||
return "charging"
|
||||
elif avg_change < -0.05:
|
||||
return "depleting"
|
||||
else:
|
||||
return "stable"
|
||||
|
||||
@staticmethod
|
||||
def is_fuel_critical(telemetry: List[EnrichedTelemetryWebhook]) -> bool:
|
||||
"""
|
||||
Check if fuel situation is critical.
|
||||
|
||||
Args:
|
||||
telemetry: List of enriched telemetry records
|
||||
|
||||
Returns:
|
||||
True if fuel optimization score is below 0.7
|
||||
"""
|
||||
if not telemetry:
|
||||
return False
|
||||
|
||||
# Check most recent telemetry
|
||||
latest = max(telemetry, key=lambda x: x.lap)
|
||||
return latest.fuel_optimization_score < 0.7
|
||||
|
||||
@staticmethod
|
||||
def assess_driver_form(telemetry: List[EnrichedTelemetryWebhook]) -> str:
|
||||
"""
|
||||
Assess driver consistency form.
|
||||
|
||||
Args:
|
||||
telemetry: List of enriched telemetry records
|
||||
|
||||
Returns:
|
||||
Form description: "excellent", "good", "inconsistent"
|
||||
"""
|
||||
if not telemetry:
|
||||
return "good"
|
||||
|
||||
# Get average consistency
|
||||
avg_consistency = sum(t.driver_consistency for t in telemetry) / len(telemetry)
|
||||
|
||||
if avg_consistency >= 0.85:
|
||||
return "excellent"
|
||||
elif avg_consistency >= 0.75:
|
||||
return "good"
|
||||
else:
|
||||
return "inconsistent"
|
||||
|
||||
@staticmethod
|
||||
def project_tire_cliff(
|
||||
telemetry: List[EnrichedTelemetryWebhook],
|
||||
current_lap: int
|
||||
) -> int:
|
||||
"""
|
||||
Project when tire degradation will hit 0.85 (performance cliff).
|
||||
|
||||
Args:
|
||||
telemetry: List of enriched telemetry records
|
||||
current_lap: Current lap number
|
||||
|
||||
Returns:
|
||||
Projected lap number when cliff will be reached
|
||||
"""
|
||||
if not telemetry:
|
||||
return current_lap + 20 # Default assumption
|
||||
|
||||
# Get current degradation and rate
|
||||
latest = max(telemetry, key=lambda x: x.lap)
|
||||
current_deg = latest.tire_degradation_index
|
||||
|
||||
if current_deg >= 0.85:
|
||||
return current_lap # Already at cliff
|
||||
|
||||
# Calculate rate
|
||||
rate = TelemetryAnalyzer.calculate_tire_degradation_rate(telemetry)
|
||||
|
||||
if rate <= 0:
|
||||
return current_lap + 50 # Not degrading, far future
|
||||
|
||||
# Project laps until 0.85
|
||||
laps_until_cliff = (0.85 - current_deg) / rate
|
||||
projected_lap = current_lap + int(laps_until_cliff)
|
||||
|
||||
return projected_lap
|
||||
|
||||
@staticmethod
|
||||
def generate_telemetry_summary(telemetry: List[EnrichedTelemetryWebhook]) -> str:
|
||||
"""
|
||||
Generate human-readable summary of telemetry trends.
|
||||
|
||||
Args:
|
||||
telemetry: List of enriched telemetry records
|
||||
|
||||
Returns:
|
||||
Summary string
|
||||
"""
|
||||
if not telemetry:
|
||||
return "No telemetry data available."
|
||||
|
||||
tire_rate = TelemetryAnalyzer.calculate_tire_degradation_rate(telemetry)
|
||||
aero_avg = TelemetryAnalyzer.calculate_aero_efficiency_avg(telemetry)
|
||||
ers_pattern = TelemetryAnalyzer.analyze_ers_pattern(telemetry)
|
||||
fuel_critical = TelemetryAnalyzer.is_fuel_critical(telemetry)
|
||||
driver_form = TelemetryAnalyzer.assess_driver_form(telemetry)
|
||||
|
||||
latest = max(telemetry, key=lambda x: x.lap)
|
||||
|
||||
summary = f"""Telemetry Analysis (Last {len(telemetry)} laps):
|
||||
- Tire degradation: {latest.tire_degradation_index:.2f} index, increasing at {tire_rate:.3f}/lap
|
||||
- Aero efficiency: {aero_avg:.2f} average
|
||||
- ERS: {latest.ers_charge:.2f} charge, {ers_pattern}
|
||||
- Fuel: {latest.fuel_optimization_score:.2f} score, {'CRITICAL' if fuel_critical else 'OK'}
|
||||
- Driver form: {driver_form} ({latest.driver_consistency:.2f} consistency)
|
||||
- Weather impact: {latest.weather_impact}"""
|
||||
|
||||
return summary
|
||||
Reference in New Issue
Block a user