2025-10-18 22:36:20 -05:00
|
|
|
"""
|
|
|
|
|
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:
|
2025-10-19 03:57:03 -05:00
|
|
|
"""Analyzes enriched lap-level telemetry data to extract trends and insights."""
|
2025-10-18 22:36:20 -05:00
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def calculate_tire_degradation_rate(telemetry: List[EnrichedTelemetryWebhook]) -> float:
|
|
|
|
|
"""
|
2025-10-19 03:57:03 -05:00
|
|
|
Calculate tire degradation rate per lap (using lap-level data).
|
2025-10-18 22:36:20 -05:00
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
telemetry: List of enriched telemetry records
|
|
|
|
|
|
|
|
|
|
Returns:
|
2025-10-19 03:57:03 -05:00
|
|
|
Latest tire degradation rate (0.0 to 1.0)
|
2025-10-18 22:36:20 -05:00
|
|
|
"""
|
|
|
|
|
if not telemetry:
|
|
|
|
|
return 0.0
|
|
|
|
|
|
2025-10-19 03:57:03 -05:00
|
|
|
# Use latest tire degradation rate from enrichment
|
2025-10-18 22:36:20 -05:00
|
|
|
latest = max(telemetry, key=lambda x: x.lap)
|
2025-10-19 03:57:03 -05:00
|
|
|
return latest.tire_degradation_rate
|
2025-10-18 22:36:20 -05:00
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def project_tire_cliff(
|
|
|
|
|
telemetry: List[EnrichedTelemetryWebhook],
|
|
|
|
|
current_lap: int
|
|
|
|
|
) -> int:
|
|
|
|
|
"""
|
2025-10-19 03:57:03 -05:00
|
|
|
Project when tire cliff will be reached (using lap-level data).
|
2025-10-18 22:36:20 -05:00
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
telemetry: List of enriched telemetry records
|
|
|
|
|
current_lap: Current lap number
|
|
|
|
|
|
|
|
|
|
Returns:
|
2025-10-19 03:57:03 -05:00
|
|
|
Estimated lap number when cliff will be reached
|
2025-10-18 22:36:20 -05:00
|
|
|
"""
|
|
|
|
|
if not telemetry:
|
|
|
|
|
return current_lap + 20 # Default assumption
|
|
|
|
|
|
2025-10-19 03:57:03 -05:00
|
|
|
# Use tire cliff risk from enrichment
|
2025-10-18 22:36:20 -05:00
|
|
|
latest = max(telemetry, key=lambda x: x.lap)
|
2025-10-19 03:57:03 -05:00
|
|
|
cliff_risk = latest.tire_cliff_risk
|
2025-10-18 22:36:20 -05:00
|
|
|
|
2025-10-19 03:57:03 -05:00
|
|
|
if cliff_risk >= 0.7:
|
|
|
|
|
return current_lap + 2 # Imminent cliff
|
|
|
|
|
elif cliff_risk >= 0.4:
|
|
|
|
|
return current_lap + 5 # Approaching cliff
|
|
|
|
|
else:
|
|
|
|
|
# Estimate based on optimal pit window
|
|
|
|
|
pit_window = latest.optimal_pit_window
|
|
|
|
|
return pit_window[1] if pit_window else current_lap + 15
|