the behemoth
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -13,21 +13,34 @@ def normalize_telemetry(payload: Dict[str, Any]) -> Dict[str, Any]:
|
||||
- tire_compound: Compound, TyreCompound, Tire
|
||||
- fuel_level: Fuel, FuelRel, FuelLevel
|
||||
- ers: ERS, ERSCharge
|
||||
- track_temp: TrackTemp
|
||||
- track_temp: TrackTemp, track_temperature
|
||||
- rain_probability: RainProb, PrecipProb
|
||||
- lap: Lap, LapNumber
|
||||
- lap: Lap, LapNumber, lap_number
|
||||
- total_laps: TotalLaps, total_laps
|
||||
- track_name: TrackName, track_name, Circuit
|
||||
- driver_name: DriverName, driver_name, Driver
|
||||
- current_position: Position, current_position
|
||||
- tire_life_laps: TireAge, tire_age, tire_life_laps
|
||||
- rainfall: Rainfall, rainfall, Rain
|
||||
|
||||
Values are clamped and defaulted if missing.
|
||||
"""
|
||||
aliases = {
|
||||
"lap": ["lap", "Lap", "LapNumber"],
|
||||
"lap": ["lap", "Lap", "LapNumber", "lap_number"],
|
||||
"speed": ["speed", "Speed"],
|
||||
"throttle": ["throttle", "Throttle"],
|
||||
"brake": ["brake", "Brake", "Brakes"],
|
||||
"tire_compound": ["tire_compound", "Compound", "TyreCompound", "Tire"],
|
||||
"fuel_level": ["fuel_level", "Fuel", "FuelRel", "FuelLevel"],
|
||||
"ers": ["ers", "ERS", "ERSCharge"],
|
||||
"track_temp": ["track_temp", "TrackTemp"],
|
||||
"track_temp": ["track_temp", "TrackTemp", "track_temperature"],
|
||||
"rain_probability": ["rain_probability", "RainProb", "PrecipProb"],
|
||||
"total_laps": ["total_laps", "TotalLaps"],
|
||||
"track_name": ["track_name", "TrackName", "Circuit"],
|
||||
"driver_name": ["driver_name", "DriverName", "Driver"],
|
||||
"current_position": ["current_position", "Position"],
|
||||
"tire_life_laps": ["tire_life_laps", "TireAge", "tire_age"],
|
||||
"rainfall": ["rainfall", "Rainfall", "Rain"],
|
||||
}
|
||||
|
||||
out: Dict[str, Any] = {}
|
||||
@@ -99,5 +112,39 @@ def normalize_telemetry(payload: Dict[str, Any]) -> Dict[str, Any]:
|
||||
out["track_temp"] = track_temp
|
||||
if rain_prob is not None:
|
||||
out["rain_probability"] = rain_prob
|
||||
|
||||
# Add race context fields if present
|
||||
total_laps = pick("total_laps", None)
|
||||
if total_laps is not None:
|
||||
try:
|
||||
out["total_laps"] = int(total_laps)
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
|
||||
track_name = pick("track_name", None)
|
||||
if track_name:
|
||||
out["track_name"] = str(track_name)
|
||||
|
||||
driver_name = pick("driver_name", None)
|
||||
if driver_name:
|
||||
out["driver_name"] = str(driver_name)
|
||||
|
||||
current_position = pick("current_position", None)
|
||||
if current_position is not None:
|
||||
try:
|
||||
out["current_position"] = int(current_position)
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
|
||||
tire_life_laps = pick("tire_life_laps", None)
|
||||
if tire_life_laps is not None:
|
||||
try:
|
||||
out["tire_life_laps"] = int(tire_life_laps)
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
|
||||
rainfall = pick("rainfall", None)
|
||||
if rainfall is not None:
|
||||
out["rainfall"] = bool(rainfall)
|
||||
|
||||
return out
|
||||
|
||||
@@ -36,30 +36,34 @@ class EnrichedRecord(BaseModel):
|
||||
|
||||
@app.post("/ingest/telemetry")
|
||||
async def ingest_telemetry(payload: Dict[str, Any] = Body(...)):
|
||||
"""Receive raw telemetry (from Pi), normalize, enrich, return enriched.
|
||||
"""Receive raw telemetry (from Pi), normalize, enrich, return enriched with race context.
|
||||
|
||||
Optionally forwards to NEXT_STAGE_CALLBACK_URL if set.
|
||||
"""
|
||||
try:
|
||||
normalized = normalize_telemetry(payload)
|
||||
enriched = _enricher.enrich(normalized)
|
||||
result = _enricher.enrich_with_context(normalized)
|
||||
enriched = result["enriched_telemetry"]
|
||||
race_context = result["race_context"]
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=f"Failed to enrich: {e}")
|
||||
|
||||
# Store enriched telemetry in recent buffer
|
||||
_recent.append(enriched)
|
||||
if len(_recent) > _MAX_RECENT:
|
||||
del _recent[: len(_recent) - _MAX_RECENT]
|
||||
|
||||
# Async forward to next stage if configured
|
||||
# Send both enriched telemetry and race context
|
||||
if _CALLBACK_URL:
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=5.0) as client:
|
||||
await client.post(_CALLBACK_URL, json=enriched)
|
||||
await client.post(_CALLBACK_URL, json=result)
|
||||
except Exception:
|
||||
# Don't fail ingestion if forwarding fails; log could be added here
|
||||
pass
|
||||
|
||||
return JSONResponse(enriched)
|
||||
return JSONResponse(result)
|
||||
|
||||
|
||||
@app.post("/enriched")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict, Any, Optional
|
||||
from typing import Dict, Any, Optional, List
|
||||
import math
|
||||
|
||||
|
||||
@@ -17,17 +17,32 @@ import math
|
||||
# "ers": 0.72, # optional 0..1
|
||||
# "track_temp": 38, # optional Celsius
|
||||
# "rain_probability": 0.2 # optional 0..1
|
||||
#
|
||||
# # Additional fields for race context:
|
||||
# "track_name": "Monza", # optional
|
||||
# "total_laps": 51, # optional
|
||||
# "driver_name": "Alonso", # optional
|
||||
# "current_position": 5, # optional
|
||||
# "tire_life_laps": 12, # optional (tire age)
|
||||
# "rainfall": False # optional (boolean)
|
||||
# }
|
||||
#
|
||||
# Output enrichment:
|
||||
# Output enrichment + race context:
|
||||
# {
|
||||
# "lap": 27,
|
||||
# "aero_efficiency": 0.83, # 0..1
|
||||
# "tire_degradation_index": 0.65, # 0..1 (higher=worse)
|
||||
# "ers_charge": 0.72, # 0..1
|
||||
# "fuel_optimization_score": 0.91, # 0..1
|
||||
# "driver_consistency": 0.89, # 0..1
|
||||
# "weather_impact": "low|medium|high"
|
||||
# "enriched_telemetry": {
|
||||
# "lap": 27,
|
||||
# "aero_efficiency": 0.83,
|
||||
# "tire_degradation_index": 0.65,
|
||||
# "ers_charge": 0.72,
|
||||
# "fuel_optimization_score": 0.91,
|
||||
# "driver_consistency": 0.89,
|
||||
# "weather_impact": "low|medium|high"
|
||||
# },
|
||||
# "race_context": {
|
||||
# "race_info": {...},
|
||||
# "driver_state": {...},
|
||||
# "competitors": [...]
|
||||
# }
|
||||
# }
|
||||
|
||||
|
||||
@@ -46,6 +61,13 @@ class EnricherState:
|
||||
lap_speeds: Dict[int, float] = field(default_factory=dict)
|
||||
lap_throttle_avg: Dict[int, float] = field(default_factory=dict)
|
||||
cumulative_wear: float = 0.0 # 0..1 approx
|
||||
|
||||
# Race context state
|
||||
track_name: str = "Unknown Circuit"
|
||||
total_laps: int = 50
|
||||
driver_name: str = "Driver"
|
||||
current_position: int = 10
|
||||
tire_compound_history: List[str] = field(default_factory=list)
|
||||
|
||||
|
||||
class Enricher:
|
||||
@@ -60,6 +82,7 @@ class Enricher:
|
||||
|
||||
# --- Public API ---
|
||||
def enrich(self, telemetry: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Legacy method - returns only enriched telemetry metrics."""
|
||||
lap = int(telemetry.get("lap", 0))
|
||||
speed = float(telemetry.get("speed", 0.0))
|
||||
throttle = float(telemetry.get("throttle", 0.0))
|
||||
@@ -90,6 +113,186 @@ class Enricher:
|
||||
"driver_consistency": round(consistency, 3),
|
||||
"weather_impact": weather_impact,
|
||||
}
|
||||
|
||||
def enrich_with_context(self, telemetry: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Enrich telemetry and build complete race context for AI layer."""
|
||||
# Extract all fields
|
||||
lap = int(telemetry.get("lap", telemetry.get("lap_number", 0)))
|
||||
speed = float(telemetry.get("speed", 0.0))
|
||||
throttle = float(telemetry.get("throttle", 0.0))
|
||||
brake = float(telemetry.get("brake", 0.0))
|
||||
tire_compound = str(telemetry.get("tire_compound", "medium")).lower()
|
||||
fuel_level = float(telemetry.get("fuel_level", 0.5))
|
||||
ers = telemetry.get("ers")
|
||||
track_temp = telemetry.get("track_temp", telemetry.get("track_temperature"))
|
||||
rain_prob = telemetry.get("rain_probability")
|
||||
rainfall = telemetry.get("rainfall", False)
|
||||
|
||||
# Race context fields
|
||||
track_name = telemetry.get("track_name", self.state.track_name)
|
||||
total_laps = int(telemetry.get("total_laps", self.state.total_laps))
|
||||
driver_name = telemetry.get("driver_name", self.state.driver_name)
|
||||
current_position = int(telemetry.get("current_position", self.state.current_position))
|
||||
tire_life_laps = int(telemetry.get("tire_life_laps", 0))
|
||||
|
||||
# Update state with race context
|
||||
if track_name:
|
||||
self.state.track_name = track_name
|
||||
if total_laps:
|
||||
self.state.total_laps = total_laps
|
||||
if driver_name:
|
||||
self.state.driver_name = driver_name
|
||||
if current_position:
|
||||
self.state.current_position = current_position
|
||||
|
||||
# Track tire compound changes
|
||||
if tire_compound and (not self.state.tire_compound_history or
|
||||
self.state.tire_compound_history[-1] != tire_compound):
|
||||
self.state.tire_compound_history.append(tire_compound)
|
||||
|
||||
# Update per-lap aggregates
|
||||
self._update_lap_stats(lap, speed, throttle)
|
||||
|
||||
# Compute enriched metrics
|
||||
aero_eff = self._compute_aero_efficiency(speed, throttle, brake)
|
||||
tire_deg = self._compute_tire_degradation(lap, speed, throttle, tire_compound, track_temp)
|
||||
ers_charge = self._compute_ers_charge(ers, throttle, brake)
|
||||
fuel_opt = self._compute_fuel_optimization(fuel_level, throttle)
|
||||
consistency = self._compute_driver_consistency()
|
||||
weather_impact = self._compute_weather_impact(rain_prob, track_temp)
|
||||
|
||||
# Build enriched telemetry
|
||||
enriched_telemetry = {
|
||||
"lap": lap,
|
||||
"aero_efficiency": round(aero_eff, 3),
|
||||
"tire_degradation_index": round(tire_deg, 3),
|
||||
"ers_charge": round(ers_charge, 3),
|
||||
"fuel_optimization_score": round(fuel_opt, 3),
|
||||
"driver_consistency": round(consistency, 3),
|
||||
"weather_impact": weather_impact,
|
||||
}
|
||||
|
||||
# Build race context
|
||||
race_context = self._build_race_context(
|
||||
lap=lap,
|
||||
total_laps=total_laps,
|
||||
track_name=track_name,
|
||||
track_temp=track_temp,
|
||||
rainfall=rainfall,
|
||||
driver_name=driver_name,
|
||||
current_position=current_position,
|
||||
tire_compound=tire_compound,
|
||||
tire_life_laps=tire_life_laps,
|
||||
fuel_level=fuel_level
|
||||
)
|
||||
|
||||
return {
|
||||
"enriched_telemetry": enriched_telemetry,
|
||||
"race_context": race_context
|
||||
}
|
||||
|
||||
def _build_race_context(
|
||||
self,
|
||||
lap: int,
|
||||
total_laps: int,
|
||||
track_name: str,
|
||||
track_temp: Optional[float],
|
||||
rainfall: bool,
|
||||
driver_name: str,
|
||||
current_position: int,
|
||||
tire_compound: str,
|
||||
tire_life_laps: int,
|
||||
fuel_level: float
|
||||
) -> Dict[str, Any]:
|
||||
"""Build complete race context structure for AI layer."""
|
||||
|
||||
# Normalize tire compound for output
|
||||
tire_map = {
|
||||
"soft": "soft",
|
||||
"medium": "medium",
|
||||
"hard": "hard",
|
||||
"inter": "intermediate",
|
||||
"intermediate": "intermediate",
|
||||
"wet": "wet"
|
||||
}
|
||||
normalized_tire = tire_map.get(tire_compound.lower(), "medium")
|
||||
|
||||
# Determine weather condition
|
||||
if rainfall:
|
||||
weather_condition = "Wet"
|
||||
else:
|
||||
weather_condition = "Dry"
|
||||
|
||||
race_context = {
|
||||
"race_info": {
|
||||
"track_name": track_name,
|
||||
"total_laps": total_laps,
|
||||
"current_lap": lap,
|
||||
"weather_condition": weather_condition,
|
||||
"track_temp_celsius": float(track_temp) if track_temp is not None else 25.0
|
||||
},
|
||||
"driver_state": {
|
||||
"driver_name": driver_name,
|
||||
"current_position": current_position,
|
||||
"current_tire_compound": normalized_tire,
|
||||
"tire_age_laps": tire_life_laps,
|
||||
"fuel_remaining_percent": fuel_level * 100.0 # Convert 0..1 to 0..100
|
||||
},
|
||||
"competitors": self._generate_mock_competitors(current_position, normalized_tire, tire_life_laps)
|
||||
}
|
||||
|
||||
return race_context
|
||||
|
||||
def _generate_mock_competitors(
|
||||
self,
|
||||
current_position: int,
|
||||
current_tire: str,
|
||||
current_tire_age: int
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Generate realistic mock competitor data for race context."""
|
||||
competitors = []
|
||||
|
||||
# Driver names pool
|
||||
driver_names = [
|
||||
"Verstappen", "Hamilton", "Leclerc", "Perez", "Sainz",
|
||||
"Russell", "Norris", "Piastri", "Alonso", "Stroll",
|
||||
"Gasly", "Ocon", "Tsunoda", "Ricciardo", "Bottas",
|
||||
"Zhou", "Magnussen", "Hulkenberg", "Albon", "Sargeant"
|
||||
]
|
||||
|
||||
tire_compounds = ["soft", "medium", "hard"]
|
||||
|
||||
# Generate positions around the current driver (±3 positions)
|
||||
positions_to_show = []
|
||||
for offset in [-3, -2, -1, 1, 2, 3]:
|
||||
pos = current_position + offset
|
||||
if 1 <= pos <= 20 and pos != current_position:
|
||||
positions_to_show.append(pos)
|
||||
|
||||
for pos in sorted(positions_to_show):
|
||||
# Calculate gap (negative if ahead, positive if behind)
|
||||
gap_base = (pos - current_position) * 2.5 # ~2.5s per position
|
||||
gap_variation = (hash(str(pos)) % 100) / 50.0 - 1.0 # -1 to +1 variation
|
||||
gap = gap_base + gap_variation
|
||||
|
||||
# Choose tire compound (bias towards similar strategy)
|
||||
tire_choice = current_tire
|
||||
if abs(hash(str(pos)) % 3) == 0: # 33% different strategy
|
||||
tire_choice = tire_compounds[pos % 3]
|
||||
|
||||
# Tire age variation
|
||||
tire_age = max(0, current_tire_age + (hash(str(pos * 7)) % 11) - 5)
|
||||
|
||||
competitor = {
|
||||
"position": pos,
|
||||
"driver": driver_names[(pos - 1) % len(driver_names)],
|
||||
"tire_compound": tire_choice,
|
||||
"tire_age_laps": tire_age,
|
||||
"gap_seconds": round(gap, 2)
|
||||
}
|
||||
competitors.append(competitor)
|
||||
|
||||
return competitors
|
||||
|
||||
# --- Internals ---
|
||||
def _update_lap_stats(self, lap: int, speed: float, throttle: float) -> None:
|
||||
|
||||
Reference in New Issue
Block a user