From 636ddf27d44a6070e2fe493e564e5dc18dadc767 Mon Sep 17 00:00:00 2001 From: Aditya Pulipaka Date: Sun, 19 Oct 2025 03:57:03 -0500 Subject: [PATCH] pipeline works from pi simulation to control output and strategy generation. --- CHANGES_SUMMARY.md | 230 -------- COMPLETION_REPORT.md | 238 -------- INTEGRATION_UPDATES.md | 262 --------- QUICK_REFERENCE.md | 213 ------- ai_intelligence_layer/ARCHITECTURE.md | 333 ----------- ai_intelligence_layer/FAST_MODE.md | 207 ------- .../IMPLEMENTATION_SUMMARY.md | 381 ------------ ai_intelligence_layer/QUICKSTART.md | 131 ----- ai_intelligence_layer/RACE_CONTEXT.md | 294 ---------- ai_intelligence_layer/RUN_SERVICES.md | 290 ---------- ai_intelligence_layer/STATUS.md | 236 -------- ai_intelligence_layer/TESTING.md | 219 ------- ai_intelligence_layer/TIMEOUT_FIX.md | 179 ------ ai_intelligence_layer/WEBHOOK_INTEGRATION.md | 316 ---------- ai_intelligence_layer/WEBHOOK_SUMMARY.md | 200 ------- .../__pycache__/config.cpython-313.pyc | Bin 1833 -> 1819 bytes .../__pycache__/main.cpython-313.pyc | Bin 7304 -> 21540 bytes ai_intelligence_layer/main.py | 281 ++++++++- .../__pycache__/input_models.cpython-313.pyc | Bin 6306 -> 6713 bytes .../__pycache__/output_models.cpython-313.pyc | Bin 7276 -> 7262 bytes ai_intelligence_layer/models/input_models.py | 13 +- .../brainstorm_prompt.cpython-313.pyc | Bin 11563 -> 11478 bytes .../prompts/brainstorm_prompt.py | 63 +- .../__pycache__/gemini_client.cpython-313.pyc | Bin 6720 -> 6706 bytes .../strategy_generator.cpython-313.pyc | Bin 4192 -> 3499 bytes .../telemetry_client.cpython-313.pyc | Bin 4963 -> 4949 bytes .../services/strategy_generator.py | 13 +- ai_intelligence_layer/start_ai_layer.sh | 45 ++ .../telemetry_buffer.cpython-313.pyc | Bin 3325 -> 3311 bytes .../__pycache__/validators.cpython-313.pyc | Bin 11377 -> 5872 bytes ai_intelligence_layer/utils/validators.py | 180 +----- hpcsim/__pycache__/__init__.cpython-313.pyc | Bin 194 -> 180 bytes hpcsim/__pycache__/adapter.cpython-313.pyc | Bin 3427 -> 3600 bytes hpcsim/__pycache__/api.cpython-313.pyc | Bin 4224 -> 4776 bytes hpcsim/__pycache__/enrichment.cpython-313.pyc | Bin 8159 -> 9497 bytes hpcsim/adapter.py | 168 ++---- hpcsim/api.py | 25 +- hpcsim/enrichment.py | 540 +++++++----------- requirements.txt | 5 +- scripts/simulate_pi_stream.py | 147 +++-- scripts/simulate_pi_websocket.py | 403 +++++++++++++ scripts/test_websocket.py | 157 +++++ 42 files changed, 1297 insertions(+), 4472 deletions(-) delete mode 100644 CHANGES_SUMMARY.md delete mode 100644 COMPLETION_REPORT.md delete mode 100644 INTEGRATION_UPDATES.md delete mode 100644 QUICK_REFERENCE.md delete mode 100644 ai_intelligence_layer/ARCHITECTURE.md delete mode 100644 ai_intelligence_layer/FAST_MODE.md delete mode 100644 ai_intelligence_layer/IMPLEMENTATION_SUMMARY.md delete mode 100644 ai_intelligence_layer/QUICKSTART.md delete mode 100644 ai_intelligence_layer/RACE_CONTEXT.md delete mode 100644 ai_intelligence_layer/RUN_SERVICES.md delete mode 100644 ai_intelligence_layer/STATUS.md delete mode 100644 ai_intelligence_layer/TESTING.md delete mode 100644 ai_intelligence_layer/TIMEOUT_FIX.md delete mode 100644 ai_intelligence_layer/WEBHOOK_INTEGRATION.md delete mode 100644 ai_intelligence_layer/WEBHOOK_SUMMARY.md create mode 100644 ai_intelligence_layer/start_ai_layer.sh create mode 100644 scripts/simulate_pi_websocket.py create mode 100644 scripts/test_websocket.py diff --git a/CHANGES_SUMMARY.md b/CHANGES_SUMMARY.md deleted file mode 100644 index 97e410d..0000000 --- a/CHANGES_SUMMARY.md +++ /dev/null @@ -1,230 +0,0 @@ -# Summary of Changes - -## Task 1: Auto-Triggering Strategy Brainstorming - -### Problem -The AI Intelligence Layer required manual API calls to `/api/strategy/brainstorm` endpoint. The webhook endpoint only received enriched telemetry without race context. - -### Solution -Modified `/api/ingest/enriched` endpoint to: -1. Accept both enriched telemetry AND race context -2. Automatically trigger strategy brainstorming when buffer has ≥3 laps -3. Return generated strategies in the webhook response - -### Files Changed -- `ai_intelligence_layer/models/input_models.py`: Added `EnrichedTelemetryWithContext` model -- `ai_intelligence_layer/main.py`: Updated webhook endpoint to auto-trigger brainstorm - -### Key Code Changes - -**New Input Model:** -```python -class EnrichedTelemetryWithContext(BaseModel): - enriched_telemetry: EnrichedTelemetryWebhook - race_context: RaceContext -``` - -**Updated Endpoint Logic:** -```python -@app.post("/api/ingest/enriched") -async def ingest_enriched_telemetry(data: EnrichedTelemetryWithContext): - # Store telemetry and race context - telemetry_buffer.add(data.enriched_telemetry) - current_race_context = data.race_context - - # Auto-trigger brainstorm when we have enough data - if buffer_data and len(buffer_data) >= 3: - response = await strategy_generator.generate( - enriched_telemetry=buffer_data, - race_context=data.race_context - ) - return { - "status": "received_and_processed", - "strategies": [s.model_dump() for s in response.strategies] - } -``` - ---- - -## Task 2: Enrichment Stage Outputs Complete Race Context - -### Problem -The enrichment service only output 7 enriched telemetry fields. The AI Intelligence Layer needed complete race context including race_info, driver_state, and competitors. - -### Solution -Extended enrichment to build and output complete race context alongside enriched telemetry metrics. - -### Files Changed -- `hpcsim/enrichment.py`: Added `enrich_with_context()` method and race context building -- `hpcsim/adapter.py`: Extended normalization for race context fields -- `hpcsim/api.py`: Updated endpoint to use new enrichment method -- `scripts/simulate_pi_stream.py`: Added race context fields to telemetry -- `scripts/enrich_telemetry.py`: Added `--full-context` flag - -### Key Code Changes - -**New Enricher Method:** -```python -def enrich_with_context(self, telemetry: Dict[str, Any]) -> Dict[str, Any]: - # Compute enriched metrics (existing logic) - enriched_telemetry = {...} - - # Build race context - race_context = { - "race_info": { - "track_name": track_name, - "total_laps": total_laps, - "current_lap": lap, - "weather_condition": weather_condition, - "track_temp_celsius": track_temp - }, - "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 - }, - "competitors": self._generate_mock_competitors(...) - } - - return { - "enriched_telemetry": enriched_telemetry, - "race_context": race_context - } -``` - -**Updated API Endpoint:** -```python -@app.post("/ingest/telemetry") -async def ingest_telemetry(payload: Dict[str, Any] = Body(...)): - normalized = normalize_telemetry(payload) - result = _enricher.enrich_with_context(normalized) # New method - - # Forward to AI layer with complete context - if _CALLBACK_URL: - await client.post(_CALLBACK_URL, json=result) - - return JSONResponse(result) -``` - ---- - -## Additional Features - -### Competitor Generation -Mock competitor data is generated for testing purposes: -- Positions around the driver (±3 positions) -- Realistic gaps based on position delta -- Varied tire strategies and ages -- Driver names from F1 roster - -### Data Normalization -Extended adapter to handle multiple field aliases: -- `lap_number` → `lap` -- `track_temperature` → `track_temp` -- `tire_life_laps` → handled correctly -- Fuel level conversion: 0-1 range → 0-100 percentage - -### Backward Compatibility -- Legacy `enrich()` method still available -- Manual `/api/strategy/brainstorm` endpoint still works -- Scripts work with or without race context fields - ---- - -## Testing - -### Unit Tests -- `tests/test_enrichment.py`: Tests for new `enrich_with_context()` method -- `tests/test_integration.py`: End-to-end integration tests - -### Integration Test -- `test_integration_live.py`: Live test script for running services - -All tests pass ✅ - ---- - -## Data Flow - -### Before: -``` -Pi → Enrichment → AI Layer (manual brainstorm call) - (7 metrics) (requires race_context from somewhere) -``` - -### After: -``` -Pi → Enrichment → AI Layer (auto-brainstorm) - (raw + context) (enriched + context) - ↓ - Strategies -``` - ---- - -## Usage Example - -**1. Start Services:** -```bash -# Terminal 1: Enrichment Service -export NEXT_STAGE_CALLBACK_URL=http://localhost:9000/api/ingest/enriched -uvicorn hpcsim.api:app --port 8000 - -# Terminal 2: AI Intelligence Layer -cd ai_intelligence_layer -uvicorn main:app --port 9000 -``` - -**2. Stream Telemetry:** -```bash -python scripts/simulate_pi_stream.py \ - --data ALONSO_2023_MONZA_RACE \ - --endpoint http://localhost:8000/ingest/telemetry \ - --speed 10.0 -``` - -**3. Observe:** -- Enrichment service processes telemetry + builds race context -- Webhooks sent to AI layer with complete data -- AI layer auto-generates strategies (after lap 3) -- Strategies returned in webhook response - ---- - -## Verification - -Run the live integration test: -```bash -python test_integration_live.py -``` - -This will: -1. Check both services are running -2. Send 5 laps of telemetry with race context -3. Verify enrichment output structure -4. Test manual brainstorm endpoint -5. Display sample strategy output - ---- - -## Benefits - -✅ **Automatic Processing**: No manual endpoint calls needed -✅ **Complete Context**: All required data in one webhook -✅ **Real-time**: Strategies generated as telemetry arrives -✅ **Stateful**: Enricher maintains race state across laps -✅ **Type-Safe**: Pydantic models ensure data validity -✅ **Backward Compatible**: Existing code continues to work -✅ **Well-Tested**: Comprehensive unit and integration tests - ---- - -## Next Steps (Optional Enhancements) - -1. **Real Competitor Data**: Replace mock competitor generation with actual race data -2. **Position Tracking**: Track position changes over laps -3. **Strategy Caching**: Cache generated strategies to avoid regeneration -4. **Webhooks Metrics**: Add monitoring for webhook delivery success -5. **Database Storage**: Persist enriched telemetry and strategies diff --git a/COMPLETION_REPORT.md b/COMPLETION_REPORT.md deleted file mode 100644 index 84bbcf3..0000000 --- a/COMPLETION_REPORT.md +++ /dev/null @@ -1,238 +0,0 @@ -# ✅ IMPLEMENTATION COMPLETE - -## Tasks Completed - -### ✅ Task 1: Auto-Trigger Strategy Brainstorming -**Requirement:** The AI Intelligence Layer's `/api/ingest/enriched` endpoint should receive `race_context` and `enriched_telemetry`, and periodically call the brainstorm logic automatically. - -**Implementation:** -- Updated `/api/ingest/enriched` endpoint to accept `EnrichedTelemetryWithContext` model -- Automatically triggers strategy brainstorming when buffer has ≥3 laps of data -- Returns generated strategies in webhook response -- No manual endpoint calls needed - -**Files Modified:** -- `ai_intelligence_layer/models/input_models.py` - Added `EnrichedTelemetryWithContext` model -- `ai_intelligence_layer/main.py` - Updated webhook endpoint with auto-brainstorm logic - ---- - -### ✅ Task 2: Complete Race Context Output -**Requirement:** The enrichment stage should output all data expected by the AI Intelligence Layer, including `race_context` (race_info, driver_state, competitors). - -**Implementation:** -- Added `enrich_with_context()` method to Enricher class -- Builds complete race context from available telemetry data -- Outputs both enriched telemetry (7 metrics) AND race context -- Webhook forwards complete payload to AI layer - -**Files Modified:** -- `hpcsim/enrichment.py` - Added `enrich_with_context()` method and race context building -- `hpcsim/adapter.py` - Extended field normalization for race context fields -- `hpcsim/api.py` - Updated to use new enrichment method -- `scripts/simulate_pi_stream.py` - Added race context fields to telemetry -- `scripts/enrich_telemetry.py` - Added `--full-context` flag - ---- - -## Verification Results - -### ✅ All Tests Pass (6/6) -``` -tests/test_enrichment.py::test_basic_ranges PASSED -tests/test_enrichment.py::test_enrich_with_context PASSED -tests/test_enrichment.py::test_stateful_wear_increases PASSED -tests/test_integration.py::test_fuel_level_conversion PASSED -tests/test_integration.py::test_pi_to_enrichment_flow PASSED -tests/test_integration.py::test_webhook_payload_structure PASSED -``` - -### ✅ Integration Validation Passed -``` -✅ Task 1: AI layer webhook receives enriched_telemetry + race_context -✅ Task 2: Enrichment outputs all expected fields -✅ All data transformations working correctly -✅ All pieces fit together properly -``` - -### ✅ No Syntax Errors -All Python files compile successfully. - ---- - -## Data Flow (Verified) - -``` -Pi Simulator (raw telemetry + race context) - ↓ -Enrichment Service (:8000) - • Normalize telemetry - • Compute 7 enriched metrics - • Build race context - ↓ -AI Intelligence Layer (:9000) via webhook - • Store enriched_telemetry - • Update race_context - • Auto-brainstorm (≥3 laps) - • Return strategies -``` - ---- - -## Output Structure (Verified) - -### Enrichment → AI Layer Webhook -```json -{ - "enriched_telemetry": { - "lap": 15, - "aero_efficiency": 0.633, - "tire_degradation_index": 0.011, - "ers_charge": 0.57, - "fuel_optimization_score": 0.76, - "driver_consistency": 1.0, - "weather_impact": "low" - }, - "race_context": { - "race_info": { - "track_name": "Monza", - "total_laps": 51, - "current_lap": 15, - "weather_condition": "Dry", - "track_temp_celsius": 42.5 - }, - "driver_state": { - "driver_name": "Alonso", - "current_position": 5, - "current_tire_compound": "medium", - "tire_age_laps": 12, - "fuel_remaining_percent": 65.0 - }, - "competitors": [...] - } -} -``` - -### AI Layer → Response -```json -{ - "status": "received_and_processed", - "lap": 15, - "buffer_size": 15, - "strategies_generated": 20, - "strategies": [...] -} -``` - ---- - -## Key Features Implemented - -✅ **Automatic Processing** -- No manual endpoint calls required -- Auto-triggers after 3 laps of data - -✅ **Complete Context** -- All 7 enriched telemetry fields -- Complete race_info (track, laps, weather) -- Complete driver_state (position, tires, fuel) -- Competitor data (mock-generated) - -✅ **Data Transformations** -- Tire compound normalization (SOFT → soft, inter → intermediate) -- Fuel level conversion (0-1 → 0-100%) -- Field alias handling (lap_number → lap, etc.) - -✅ **Backward Compatibility** -- Legacy `enrich()` method still works -- Manual `/api/strategy/brainstorm` endpoint still available -- Existing tests continue to pass - -✅ **Type Safety** -- Pydantic models validate all data -- Proper error handling and fallbacks - -✅ **Well Tested** -- Unit tests for enrichment -- Integration tests for end-to-end flow -- Live validation script - ---- - -## Documentation Provided - -1. ✅ `INTEGRATION_UPDATES.md` - Detailed technical documentation -2. ✅ `CHANGES_SUMMARY.md` - Executive summary of changes -3. ✅ `QUICK_REFERENCE.md` - Quick reference guide -4. ✅ `validate_integration.py` - Comprehensive validation script -5. ✅ `test_integration_live.py` - Live service testing -6. ✅ Updated tests in `tests/` directory - ---- - -## Correctness Guarantees - -✅ **Structural Correctness** -- All required fields present in output -- Correct data types (Pydantic validation) -- Proper nesting of objects - -✅ **Data Correctness** -- Field mappings verified -- Value transformations tested -- Range validations in place - -✅ **Integration Correctness** -- End-to-end flow tested -- Webhook payload validated -- Auto-trigger logic verified - -✅ **Backward Compatibility** -- Legacy methods still work -- Existing code unaffected -- All original tests pass - ---- - -## How to Run - -### Start Services -```bash -# Terminal 1: Enrichment -export NEXT_STAGE_CALLBACK_URL=http://localhost:9000/api/ingest/enriched -uvicorn hpcsim.api:app --port 8000 - -# Terminal 2: AI Layer -cd ai_intelligence_layer && uvicorn main:app --port 9000 -``` - -### Stream Telemetry -```bash -python scripts/simulate_pi_stream.py \ - --data ALONSO_2023_MONZA_RACE \ - --endpoint http://localhost:8000/ingest/telemetry \ - --speed 10.0 -``` - -### Validate -```bash -# Unit & integration tests -python -m pytest tests/test_enrichment.py tests/test_integration.py -v - -# Comprehensive validation -python validate_integration.py -``` - ---- - -## Summary - -Both tasks have been completed successfully with: -- ✅ Correct implementation -- ✅ Comprehensive testing -- ✅ Full documentation -- ✅ Backward compatibility -- ✅ Type safety -- ✅ Verified integration - -All pieces fit together properly and work as expected! 🎉 diff --git a/INTEGRATION_UPDATES.md b/INTEGRATION_UPDATES.md deleted file mode 100644 index b00b0c7..0000000 --- a/INTEGRATION_UPDATES.md +++ /dev/null @@ -1,262 +0,0 @@ -# Integration Updates - Enrichment to AI Intelligence Layer - -## Overview -This document describes the updates made to integrate the HPC enrichment stage with the AI Intelligence Layer for automatic strategy generation. - -## Changes Summary - -### 1. AI Intelligence Layer (`/api/ingest/enriched` endpoint) - -**Previous behavior:** -- Received only enriched telemetry data -- Stored data in buffer -- Required manual calls to `/api/strategy/brainstorm` endpoint - -**New behavior:** -- Receives **both** enriched telemetry AND race context -- Stores telemetry in buffer AND updates global race context -- **Automatically triggers strategy brainstorming** when sufficient data is available (≥3 laps) -- Returns generated strategies in the webhook response - -**Updated Input Model:** -```python -class EnrichedTelemetryWithContext(BaseModel): - enriched_telemetry: EnrichedTelemetryWebhook - race_context: RaceContext -``` - -**Response includes:** -- `status`: Processing status -- `lap`: Current lap number -- `buffer_size`: Number of telemetry records in buffer -- `strategies_generated`: Number of strategies created (if auto-brainstorm triggered) -- `strategies`: List of strategy objects (if auto-brainstorm triggered) - -### 2. Enrichment Stage Output - -**Previous output (enriched telemetry only):** -```json -{ - "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" -} -``` - -**New output (enriched telemetry + race context):** -```json -{ - "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" - }, - "race_context": { - "race_info": { - "track_name": "Monza", - "total_laps": 51, - "current_lap": 27, - "weather_condition": "Dry", - "track_temp_celsius": 42.5 - }, - "driver_state": { - "driver_name": "Alonso", - "current_position": 5, - "current_tire_compound": "medium", - "tire_age_laps": 12, - "fuel_remaining_percent": 65.0 - }, - "competitors": [ - { - "position": 4, - "driver": "Sainz", - "tire_compound": "medium", - "tire_age_laps": 10, - "gap_seconds": -2.3 - }, - // ... more competitors - ] - } -} -``` - -### 3. Modified Components - -#### `hpcsim/enrichment.py` -- Added `enrich_with_context()` method (new primary method) -- Maintains backward compatibility with `enrich()` (legacy method) -- Builds complete race context including: - - Race information (track, laps, weather) - - Driver state (position, tires, fuel) - - Competitor data (mock generation for testing) - -#### `hpcsim/adapter.py` -- Extended to normalize additional fields: - - `track_name` - - `total_laps` - - `driver_name` - - `current_position` - - `tire_life_laps` - - `rainfall` - -#### `hpcsim/api.py` -- Updated `/ingest/telemetry` endpoint to use `enrich_with_context()` -- Webhook now sends complete payload with enriched telemetry + race context - -#### `scripts/simulate_pi_stream.py` -- Updated to include race context fields in telemetry data: - - `track_name`: "Monza" - - `driver_name`: "Alonso" - - `current_position`: 5 - - `fuel_level`: Calculated based on lap progress - -#### `scripts/enrich_telemetry.py` -- Added `--full-context` flag for outputting complete race context -- Default behavior unchanged (backward compatible) - -#### `ai_intelligence_layer/main.py` -- Updated `/api/ingest/enriched` endpoint to: - - Accept `EnrichedTelemetryWithContext` model - - Store race context globally - - Auto-trigger strategy brainstorming with ≥3 laps of data - - Return strategies in webhook response - -#### `ai_intelligence_layer/models/input_models.py` -- Added `EnrichedTelemetryWithContext` model - -## Usage - -### Running the Full Pipeline - -1. **Start the enrichment service:** -```bash -export NEXT_STAGE_CALLBACK_URL=http://localhost:9000/api/ingest/enriched -uvicorn hpcsim.api:app --host 0.0.0.0 --port 8000 -``` - -2. **Start the AI Intelligence Layer:** -```bash -cd ai_intelligence_layer -uvicorn main:app --host 0.0.0.0 --port 9000 -``` - -3. **Stream telemetry data:** -```bash -python scripts/simulate_pi_stream.py \ - --data ALONSO_2023_MONZA_RACE \ - --endpoint http://localhost:8000/ingest/telemetry \ - --speed 10.0 -``` - -### What Happens - -1. Pi simulator sends raw telemetry to enrichment service (port 8000) -2. Enrichment service: - - Normalizes telemetry - - Enriches with HPC metrics - - Builds race context - - Forwards to AI layer webhook (port 9000) -3. AI Intelligence Layer: - - Receives enriched telemetry + race context - - Stores in buffer - - **Automatically generates strategies** when buffer has ≥3 laps - - Returns strategies in webhook response - -### Manual Testing - -Test enrichment with context: -```bash -echo '{"lap":10,"speed":280,"throttle":0.85,"brake":0.05,"tire_compound":"medium","fuel_level":0.7,"track_temp":42.5,"total_laps":51,"track_name":"Monza","driver_name":"Alonso","current_position":5,"tire_life_laps":8}' | \ -python scripts/enrich_telemetry.py --full-context -``` - -Test webhook directly: -```bash -curl -X POST http://localhost:9000/api/ingest/enriched \ - -H "Content-Type: application/json" \ - -d '{ - "enriched_telemetry": { - "lap": 15, - "aero_efficiency": 0.85, - "tire_degradation_index": 0.3, - "ers_charge": 0.75, - "fuel_optimization_score": 0.9, - "driver_consistency": 0.88, - "weather_impact": "low" - }, - "race_context": { - "race_info": { - "track_name": "Monza", - "total_laps": 51, - "current_lap": 15, - "weather_condition": "Dry", - "track_temp_celsius": 42.5 - }, - "driver_state": { - "driver_name": "Alonso", - "current_position": 5, - "current_tire_compound": "medium", - "tire_age_laps": 10, - "fuel_remaining_percent": 70.0 - }, - "competitors": [] - } - }' -``` - -## Testing - -Run all tests: -```bash -python -m pytest tests/ -v -``` - -Specific test files: -```bash -# Unit tests for enrichment -python -m pytest tests/test_enrichment.py -v - -# Integration tests -python -m pytest tests/test_integration.py -v -``` - -## Backward Compatibility - -- The legacy `enrich()` method still works and returns only enriched metrics -- The `/api/strategy/brainstorm` endpoint can still be called manually -- Scripts work with or without race context fields -- Existing tests continue to pass - -## Key Benefits - -1. **Automatic Strategy Generation**: No manual endpoint calls needed -2. **Complete Context**: AI layer receives all necessary data in one webhook -3. **Real-time Processing**: Strategies generated as telemetry arrives -4. **Stateful Enrichment**: Enricher maintains race state across laps -5. **Realistic Competitor Data**: Mock competitors generated for testing -6. **Type Safety**: Pydantic models ensure data validity - -## Data Flow - -``` -Pi/Simulator → Enrichment Service → AI Intelligence Layer - (raw) (enrich + context) (auto-brainstorm) - ↓ - Strategies -``` - -## Notes - -- **Minimum buffer size**: AI layer waits for ≥3 laps before auto-brainstorming -- **Competitor data**: Currently mock-generated; can be replaced with real data -- **Fuel conversion**: Automatically converts 0-1 range to 0-100 percentage -- **Tire normalization**: Maps all tire compound variations to standard names -- **Weather detection**: Based on `rainfall` boolean and temperature diff --git a/QUICK_REFERENCE.md b/QUICK_REFERENCE.md deleted file mode 100644 index 7dff7d8..0000000 --- a/QUICK_REFERENCE.md +++ /dev/null @@ -1,213 +0,0 @@ -# Quick Reference: Integration Changes - -## 🎯 What Was Done - -### Task 1: Auto-Trigger Strategy Brainstorming ✅ -- **File**: `ai_intelligence_layer/main.py` -- **Endpoint**: `/api/ingest/enriched` -- **Change**: Now receives `enriched_telemetry` + `race_context` and automatically calls brainstorm logic -- **Trigger**: Auto-brainstorms when buffer has ≥3 laps -- **Output**: Returns generated strategies in webhook response - -### Task 2: Complete Race Context Output ✅ -- **File**: `hpcsim/enrichment.py` -- **Method**: New `enrich_with_context()` method -- **Output**: Both enriched telemetry (7 fields) AND race context (race_info + driver_state + competitors) -- **Integration**: Seamlessly flows from enrichment → AI layer - ---- - -## 📋 Modified Files - -### Core Changes -1. ✅ `hpcsim/enrichment.py` - Added `enrich_with_context()` method -2. ✅ `hpcsim/adapter.py` - Extended field normalization -3. ✅ `hpcsim/api.py` - Updated to output full context -4. ✅ `ai_intelligence_layer/main.py` - Auto-trigger brainstorm -5. ✅ `ai_intelligence_layer/models/input_models.py` - New webhook model - -### Supporting Changes -6. ✅ `scripts/simulate_pi_stream.py` - Added race context fields -7. ✅ `scripts/enrich_telemetry.py` - Added `--full-context` flag - -### Testing -8. ✅ `tests/test_enrichment.py` - Added context tests -9. ✅ `tests/test_integration.py` - New integration tests (3 tests) -10. ✅ `test_integration_live.py` - Live testing script - -### Documentation -11. ✅ `INTEGRATION_UPDATES.md` - Detailed documentation -12. ✅ `CHANGES_SUMMARY.md` - Executive summary - ---- - -## 🧪 Verification - -### All Tests Pass -```bash -python -m pytest tests/test_enrichment.py tests/test_integration.py -v -# Result: 6 passed in 0.01s ✅ -``` - -### No Syntax Errors -```bash -python -m py_compile hpcsim/enrichment.py hpcsim/adapter.py hpcsim/api.py -python -m py_compile ai_intelligence_layer/main.py ai_intelligence_layer/models/input_models.py -# All files compile successfully ✅ -``` - ---- - -## 🔄 Data Flow - -``` -┌─────────────────┐ -│ Pi Simulator │ -│ (Raw Data) │ -└────────┬────────┘ - │ POST /ingest/telemetry - │ {lap, speed, throttle, tire_compound, - │ total_laps, track_name, driver_name, ...} - ↓ -┌─────────────────────────────────────┐ -│ Enrichment Service (Port 8000) │ -│ • Normalize telemetry │ -│ • Compute HPC metrics │ -│ • Build race context │ -└────────┬────────────────────────────┘ - │ Webhook POST /api/ingest/enriched - │ {enriched_telemetry: {...}, race_context: {...}} - ↓ -┌─────────────────────────────────────┐ -│ AI Intelligence Layer (Port 9000) │ -│ • Store in buffer │ -│ • Update race context │ -│ • Auto-trigger brainstorm (≥3 laps)│ -│ • Generate 20 strategies │ -└────────┬────────────────────────────┘ - │ Response - │ {status, strategies: [...]} - ↓ - [Strategies Available] -``` - ---- - -## 📊 Output Structure - -### Enrichment Output -```json -{ - "enriched_telemetry": { - "lap": 15, - "aero_efficiency": 0.85, - "tire_degradation_index": 0.3, - "ers_charge": 0.75, - "fuel_optimization_score": 0.9, - "driver_consistency": 0.88, - "weather_impact": "low" - }, - "race_context": { - "race_info": { - "track_name": "Monza", - "total_laps": 51, - "current_lap": 15, - "weather_condition": "Dry", - "track_temp_celsius": 42.5 - }, - "driver_state": { - "driver_name": "Alonso", - "current_position": 5, - "current_tire_compound": "medium", - "tire_age_laps": 10, - "fuel_remaining_percent": 70.0 - }, - "competitors": [...] - } -} -``` - -### Webhook Response (from AI Layer) -```json -{ - "status": "received_and_processed", - "lap": 15, - "buffer_size": 15, - "strategies_generated": 20, - "strategies": [ - { - "strategy_id": 1, - "strategy_name": "Conservative Medium-Hard", - "stop_count": 1, - "pit_laps": [32], - "tire_sequence": ["medium", "hard"], - "brief_description": "...", - "risk_level": "low", - "key_assumption": "..." - }, - ... - ] -} -``` - ---- - -## 🚀 Quick Start - -### Start Both Services -```bash -# Terminal 1: Enrichment -export NEXT_STAGE_CALLBACK_URL=http://localhost:9000/api/ingest/enriched -uvicorn hpcsim.api:app --port 8000 - -# Terminal 2: AI Layer -cd ai_intelligence_layer && uvicorn main:app --port 9000 - -# Terminal 3: Stream Data -python scripts/simulate_pi_stream.py \ - --data ALONSO_2023_MONZA_RACE \ - --endpoint http://localhost:8000/ingest/telemetry \ - --speed 10.0 -``` - -### Watch the Magic ✨ -- Lap 1-2: Telemetry ingested, buffered -- Lap 3+: Auto-brainstorm triggered, strategies generated! -- Check AI layer logs for strategy output - ---- - -## ✅ Correctness Guarantees - -1. **Type Safety**: All data validated by Pydantic models -2. **Field Mapping**: Comprehensive alias handling in adapter -3. **Data Conversion**: Fuel 0-1 → 0-100%, tire normalization -4. **State Management**: Enricher maintains state across laps -5. **Error Handling**: Graceful fallbacks if brainstorm fails -6. **Backward Compatibility**: Legacy methods still work -7. **Test Coverage**: 6 tests covering all critical paths - ---- - -## 📌 Key Points - -✅ **Automatic**: No manual API calls needed -✅ **Complete**: All race context included -✅ **Tested**: All tests pass -✅ **Compatible**: Existing code unaffected -✅ **Documented**: Comprehensive docs provided -✅ **Correct**: Type-safe, validated data flow - ---- - -## 🎓 Implementation Notes - -- **Minimum Buffer**: Waits for 3 laps before auto-brainstorm -- **Competitors**: Mock-generated (can be replaced with real data) -- **Webhook**: Enrichment → AI layer (push model) -- **Fallback**: AI layer can still pull from enrichment service -- **State**: Enricher tracks race state, tire changes, consistency - ---- - -**Everything is working correctly and all pieces fit together! ✨** diff --git a/ai_intelligence_layer/ARCHITECTURE.md b/ai_intelligence_layer/ARCHITECTURE.md deleted file mode 100644 index ac9aae3..0000000 --- a/ai_intelligence_layer/ARCHITECTURE.md +++ /dev/null @@ -1,333 +0,0 @@ -# System Architecture & Data Flow - -## High-Level Architecture - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ F1 Race Strategy System │ -└─────────────────────────────────────────────────────────────────┘ - -┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ -│ Raw Race │ │ HPC Compute │ │ Enrichment │ -│ Telemetry │────────▶│ Cluster │────────▶│ Module │ -│ │ │ │ │ (port 8000) │ -└─────────────────┘ └─────────────────┘ └────────┬────────┘ - │ - │ POST webhook - │ (enriched data) - │ - ▼ - ┌─────────────────────────────────────────────┐ - │ AI Intelligence Layer (port 9000) │ - │ ┌─────────────────────────────────────┐ │ - │ │ Step 1: Strategy Brainstorming │ │ - │ │ - Generate 20 diverse strategies │ │ - │ │ - Temperature: 0.9 (creative) │ │ - │ └─────────────────────────────────────┘ │ - │ │ │ - │ ▼ │ - │ ┌─────────────────────────────────────┐ │ - │ │ Step 2: Strategy Analysis │ │ - │ │ - Select top 3 strategies │ │ - │ │ - Temperature: 0.3 (analytical) │ │ - │ └─────────────────────────────────────┘ │ - │ │ - │ Powered by: Google Gemini 1.5 Pro │ - └──────────────────┬──────────────────────────┘ - │ - │ Strategic recommendations - │ - ▼ - ┌─────────────────────────────────────────┐ - │ Race Engineers / Frontend │ - │ - Win probabilities │ - │ - Risk assessments │ - │ - Engineer briefs │ - │ - Driver radio scripts │ - │ - ECU commands │ - └─────────────────────────────────────────┘ -``` - -## Data Flow - Detailed - -``` -1. ENRICHED TELEMETRY INPUT -┌────────────────────────────────────────────────────────────────┐ -│ { │ -│ "lap": 27, │ -│ "aero_efficiency": 0.83, // 0-1, higher = better │ -│ "tire_degradation_index": 0.65, // 0-1, higher = worse │ -│ "ers_charge": 0.72, // 0-1, energy available │ -│ "fuel_optimization_score": 0.91,// 0-1, efficiency │ -│ "driver_consistency": 0.89, // 0-1, lap-to-lap variance │ -│ "weather_impact": "medium" // low/medium/high │ -│ } │ -└────────────────────────────────────────────────────────────────┘ - │ - ▼ -2. RACE CONTEXT INPUT -┌────────────────────────────────────────────────────────────────┐ -│ { │ -│ "race_info": { │ -│ "track_name": "Monaco", │ -│ "current_lap": 27, │ -│ "total_laps": 58 │ -│ }, │ -│ "driver_state": { │ -│ "driver_name": "Hamilton", │ -│ "current_position": 4, │ -│ "current_tire_compound": "medium", │ -│ "tire_age_laps": 14 │ -│ }, │ -│ "competitors": [...] │ -│ } │ -└────────────────────────────────────────────────────────────────┘ - │ - ▼ -3. TELEMETRY ANALYSIS -┌────────────────────────────────────────────────────────────────┐ -│ • Calculate tire degradation rate: 0.030/lap │ -│ • Project tire cliff: Lap 33 │ -│ • Analyze ERS pattern: stable │ -│ • Assess fuel situation: OK │ -│ • Evaluate driver form: excellent │ -└────────────────────────────────────────────────────────────────┘ - │ - ▼ -4. STEP 1: BRAINSTORM (Gemini AI) -┌────────────────────────────────────────────────────────────────┐ -│ Temperature: 0.9 (high creativity) │ -│ Prompt includes: │ -│ • Last 10 laps telemetry │ -│ • Calculated trends │ -│ • Race constraints │ -│ • Competitor analysis │ -│ │ -│ Output: 20 diverse strategies │ -│ • Conservative (1-stop, low risk) │ -│ • Standard (balanced approach) │ -│ • Aggressive (undercut/overcut) │ -│ • Reactive (respond to competitors) │ -│ • Contingency (safety car, rain) │ -└────────────────────────────────────────────────────────────────┘ - │ - ▼ -5. STRATEGY VALIDATION -┌────────────────────────────────────────────────────────────────┐ -│ • Pit laps within valid range │ -│ • At least 2 tire compounds (F1 rule) │ -│ • Stop count matches pit laps │ -│ • Tire sequence correct length │ -└────────────────────────────────────────────────────────────────┘ - │ - ▼ -6. STEP 2: ANALYZE (Gemini AI) -┌────────────────────────────────────────────────────────────────┐ -│ Temperature: 0.3 (analytical consistency) │ -│ Analysis framework: │ -│ 1. Tire degradation projection │ -│ 2. Aero efficiency impact │ -│ 3. Fuel management │ -│ 4. Driver consistency │ -│ 5. Weather & track position │ -│ 6. Competitor analysis │ -│ │ -│ Selection criteria: │ -│ • Rank 1: RECOMMENDED (highest podium %) │ -│ • Rank 2: ALTERNATIVE (viable backup) │ -│ • Rank 3: CONSERVATIVE (safest) │ -└────────────────────────────────────────────────────────────────┘ - │ - ▼ -7. FINAL OUTPUT -┌────────────────────────────────────────────────────────────────┐ -│ For EACH of top 3 strategies: │ -│ │ -│ • Predicted Outcome │ -│ - Finish position: P3 │ -│ - P1 probability: 8% │ -│ - P2 probability: 22% │ -│ - P3 probability: 45% │ -│ - Confidence: 78% │ -│ │ -│ • Risk Assessment │ -│ - Risk level: medium │ -│ - Key risks: ["Pit under 2.5s", "Traffic"] │ -│ - Success factors: ["Tire advantage", "Window open"] │ -│ │ -│ • Telemetry Insights │ -│ - "Tire cliff at lap 35" │ -│ - "Aero 0.83 - performing well" │ -│ - "Fuel excellent, no saving" │ -│ - "Driver form excellent" │ -│ │ -│ • Engineer Brief │ -│ - Title: "Aggressive Undercut Lap 28" │ -│ - Summary: "67% chance P3 or better" │ -│ - Key points: [...] │ -│ - Execution steps: [...] │ -│ │ -│ • Driver Audio Script │ -│ "Box this lap. Softs going on. Push mode." │ -│ │ -│ • ECU Commands │ -│ - Fuel: RICH │ -│ - ERS: AGGRESSIVE_DEPLOY │ -│ - Engine: PUSH │ -│ │ -│ • Situational Context │ -│ - "Decision needed in 2 laps" │ -│ - "Tire deg accelerating" │ -└────────────────────────────────────────────────────────────────┘ -``` - -## API Endpoints Detail - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ GET /api/health │ -├─────────────────────────────────────────────────────────────────┤ -│ Purpose: Health check │ -│ Response: {status, version, demo_mode} │ -│ Latency: <100ms │ -└─────────────────────────────────────────────────────────────────┘ - -┌─────────────────────────────────────────────────────────────────┐ -│ POST /api/ingest/enriched │ -├─────────────────────────────────────────────────────────────────┤ -│ Purpose: Webhook receiver from enrichment service │ -│ Input: Single lap enriched telemetry │ -│ Action: Store in buffer (max 100 records) │ -│ Response: {status, lap, buffer_size} │ -│ Latency: <50ms │ -└─────────────────────────────────────────────────────────────────┘ - -┌─────────────────────────────────────────────────────────────────┐ -│ POST /api/strategy/brainstorm │ -├─────────────────────────────────────────────────────────────────┤ -│ Purpose: Generate 20 diverse strategies │ -│ Input: │ -│ - enriched_telemetry (optional, auto-fetch if missing) │ -│ - race_context (required) │ -│ Process: │ -│ 1. Fetch telemetry if needed │ -│ 2. Build prompt with telemetry analysis │ -│ 3. Call Gemini (temp=0.9) │ -│ 4. Parse & validate strategies │ -│ Output: {strategies: [20 strategies]} │ -│ Latency: <5s (target) │ -└─────────────────────────────────────────────────────────────────┘ - -┌─────────────────────────────────────────────────────────────────┐ -│ POST /api/strategy/analyze │ -├─────────────────────────────────────────────────────────────────┤ -│ Purpose: Analyze 20 strategies, select top 3 │ -│ Input: │ -│ - enriched_telemetry (optional, auto-fetch if missing) │ -│ - race_context (required) │ -│ - strategies (required, typically 20) │ -│ Process: │ -│ 1. Fetch telemetry if needed │ -│ 2. Build analytical prompt │ -│ 3. Call Gemini (temp=0.3) │ -│ 4. Parse nested response structures │ -│ Output: │ -│ - top_strategies: [3 detailed strategies] │ -│ - situational_context: {...} │ -│ Latency: <10s (target) │ -└─────────────────────────────────────────────────────────────────┘ -``` - -## Integration Patterns - -### Pattern 1: Pull Model -``` -Enrichment Service (8000) ←─────GET /enriched───── AI Layer (9000) - [polls periodically] -``` - -### Pattern 2: Push Model (RECOMMENDED) -``` -Enrichment Service (8000) ─────POST /ingest/enriched────▶ AI Layer (9000) - [webhook on new data] -``` - -### Pattern 3: Direct Request -``` -Client ──POST /brainstorm──▶ AI Layer (9000) - [includes telemetry] -``` - -## Error Handling Flow - -``` -Request - │ - ▼ -┌─────────────────┐ -│ Validate Input │ -└────────┬────────┘ - │ - ▼ -┌─────────────────┐ NO ┌──────────────────┐ -│ Telemetry │────────────▶│ Fetch from │ -│ Provided? │ │ localhost:8000 │ -└────────┬────────┘ └────────┬─────────┘ - YES │ │ - └───────────────┬───────────────┘ - ▼ - ┌──────────────┐ - │ Call Gemini │ - └──────┬───────┘ - │ - ┌────┴────┐ - │ Success?│ - └────┬────┘ - YES │ NO - │ │ - │ ▼ - │ ┌────────────────┐ - │ │ Retry with │ - │ │ stricter prompt│ - │ └────────┬───────┘ - │ │ - │ ┌────┴────┐ - │ │Success? │ - │ └────┬────┘ - │ YES │ NO - │ │ │ - └───────────┤ │ - │ ▼ - │ ┌────────────┐ - │ │ Return │ - │ │ Error 500 │ - │ └────────────┘ - ▼ - ┌──────────────┐ - │ Return │ - │ Success 200 │ - └──────────────┘ -``` - -## Performance Characteristics - -| Component | Target | Typical | Max | -|-----------|--------|---------|-----| -| Health check | <100ms | 50ms | 200ms | -| Webhook ingest | <50ms | 20ms | 100ms | -| Brainstorm (20 strategies) | <5s | 3-4s | 10s | -| Analyze (top 3) | <10s | 6-8s | 20s | -| Gemini API call | <3s | 2s | 8s | -| Telemetry fetch | <500ms | 200ms | 1s | - -## Scalability Considerations - -- **Concurrent Requests**: FastAPI async handles multiple simultaneously -- **Rate Limiting**: Gemini API has quotas (check your tier) -- **Caching**: Demo mode caches identical requests -- **Buffer Size**: Webhook buffer limited to 100 records -- **Memory**: ~100MB per service instance - ---- - -Built for the HPC + AI Race Strategy Hackathon 🏎️ diff --git a/ai_intelligence_layer/FAST_MODE.md b/ai_intelligence_layer/FAST_MODE.md deleted file mode 100644 index f898034..0000000 --- a/ai_intelligence_layer/FAST_MODE.md +++ /dev/null @@ -1,207 +0,0 @@ -# ⚡ SIMPLIFIED & FAST AI Layer - -## What Changed - -Simplified the entire AI flow for **ultra-fast testing and development**: - -### Before (Slow) -- Generate 20 strategies (~45-60 seconds) -- Analyze all 20 and select top 3 (~40-60 seconds) -- **Total: ~2 minutes per request** ❌ - -### After (Fast) -- Generate **1 strategy** (~5-10 seconds) -- **Skip analysis** completely -- **Total: ~10 seconds per request** ✅ - -## Configuration - -### Current Settings (`.env`) -```bash -FAST_MODE=true -STRATEGY_COUNT=1 # ⚡ Set to 1 for testing, 20 for production -``` - -### How to Adjust - -**For ultra-fast testing (current):** -```bash -STRATEGY_COUNT=1 -``` - -**For demo/showcase:** -```bash -STRATEGY_COUNT=5 -``` - -**For production:** -```bash -STRATEGY_COUNT=20 -``` - -## Simplified Workflow - -``` -┌──────────────────┐ -│ Enrichment │ -│ Service POSTs │ -│ telemetry │ -└────────┬─────────┘ - │ - ▼ -┌──────────────────┐ -│ Webhook Buffer │ -│ (stores data) │ -└────────┬─────────┘ - │ - ▼ -┌──────────────────┐ -│ Brainstorm │ ⚡ 1 strategy only! -│ (Gemini API) │ ~10 seconds -└────────┬─────────┘ - │ - ▼ -┌──────────────────┐ -│ Return Strategy │ -│ No analysis! │ -└──────────────────┘ -``` - -## Quick Test - -### 1. Push telemetry via webhook -```bash -python3 test_webhook_push.py --loop 5 -``` - -### 2. Generate strategy (fast!) -```bash -python3 test_buffer_usage.py -``` - -**Output:** -``` -Testing FAST brainstorm with buffered telemetry... -(Configured for 1 strategy only - ultra fast!) - -✓ Brainstorm succeeded! - Generated 1 strategy - - Strategy: - 1. Medium-to-Hard Standard (1-stop) - Tires: medium → hard - Optimal 1-stop at lap 32 when tire degradation reaches cliff - -✓ SUCCESS: AI layer is using webhook buffer! -``` - -**Time: ~10 seconds** instead of 2 minutes! - -## API Response Format - -### Brainstorm Response (Simplified) - -```json -{ - "strategies": [ - { - "strategy_id": 1, - "strategy_name": "Medium-to-Hard Standard", - "stop_count": 1, - "pit_laps": [32], - "tire_sequence": ["medium", "hard"], - "brief_description": "Optimal 1-stop at lap 32 when tire degradation reaches cliff", - "risk_level": "medium", - "key_assumption": "No safety car interventions" - } - ] -} -``` - -**No analysis object!** Just the strategy/strategies. - -## What Was Removed - -❌ **Analysis endpoint** - Skipped entirely for speed -❌ **Top 3 selection** - Only 1 strategy generated -❌ **Detailed rationale** - Simple description only -❌ **Risk assessment details** - Basic risk level only -❌ **Engineer briefs** - Not generated -❌ **Radio scripts** - Not generated -❌ **ECU commands** - Not generated - -## What Remains - -✅ **Webhook push** - Still works perfectly -✅ **Buffer storage** - Still stores telemetry -✅ **Strategy generation** - Just faster (1 instead of 20) -✅ **F1 rule validation** - Still validates tire compounds -✅ **Telemetry analysis** - Still calculates tire cliff, degradation - -## Re-enabling Full Mode - -When you need the complete system (for demos/production): - -### 1. Update `.env` -```bash -STRATEGY_COUNT=20 -``` - -### 2. Restart service -```bash -# Service will auto-reload if running with uvicorn --reload -# Or manually restart: -python main.py -``` - -### 3. Use analysis endpoint -```bash -# After brainstorm, call analyze with the 20 strategies -POST /api/strategy/analyze -{ - "race_context": {...}, - "strategies": [...], # 20 strategies from brainstorm - "enriched_telemetry": [...] # optional -} -``` - -## Performance Comparison - -| Mode | Strategies | Time | Use Case | -|------|-----------|------|----------| -| **Ultra Fast** | 1 | ~10s | Testing, development | -| **Fast** | 5 | ~20s | Quick demos | -| **Standard** | 10 | ~35s | Demos with variety | -| **Full** | 20 | ~60s | Production, full analysis | - -## Benefits of Simplified Flow - -✅ **Faster iteration** - Test webhook integration quickly -✅ **Lower API costs** - Fewer Gemini API calls -✅ **Easier debugging** - Simpler responses to inspect -✅ **Better dev experience** - No waiting 2 minutes per test -✅ **Still validates** - All core logic still works - -## Migration Path - -### Phase 1: Testing (Now) -- Use `STRATEGY_COUNT=1` -- Test webhook integration -- Verify telemetry flow -- Debug any issues - -### Phase 2: Demo -- Set `STRATEGY_COUNT=5` -- Show variety of strategies -- Still fast enough for live demos - -### Phase 3: Production -- Set `STRATEGY_COUNT=20` -- Enable analysis endpoint -- Full feature set - ---- - -**Current Status:** ⚡ Ultra-fast mode enabled! -**Response Time:** ~10 seconds (was ~2 minutes) -**Ready for:** Rapid testing and webhook integration validation diff --git a/ai_intelligence_layer/IMPLEMENTATION_SUMMARY.md b/ai_intelligence_layer/IMPLEMENTATION_SUMMARY.md deleted file mode 100644 index 6202343..0000000 --- a/ai_intelligence_layer/IMPLEMENTATION_SUMMARY.md +++ /dev/null @@ -1,381 +0,0 @@ -# AI Intelligence Layer - Implementation Summary - -## 🎉 PROJECT COMPLETE - -The AI Intelligence Layer has been successfully built and tested! This is the **core innovation** of your F1 race strategy system. - ---- - -## 📦 What Was Built - -### ✅ Core Components - -1. **FastAPI Service (main.py)** - - Running on port 9000 - - 4 endpoints: health, ingest webhook, brainstorm, analyze - - Full CORS support - - Comprehensive error handling - -2. **Data Models (models/)** - - `input_models.py`: Request schemas for telemetry and race context - - `output_models.py`: Response schemas with 10+ nested structures - - `internal_models.py`: Internal processing models - -3. **Gemini AI Integration (services/gemini_client.py)** - - Automatic JSON parsing with retry logic - - Error recovery with stricter prompts - - Demo mode caching for consistent results - - Configurable timeout and retry settings - -4. **Telemetry Client (services/telemetry_client.py)** - - Fetches from enrichment service (localhost:8000) - - Health check integration - - Automatic fallback handling - -5. **Strategy Services** - - `strategy_generator.py`: Brainstorm 20 diverse strategies - - `strategy_analyzer.py`: Select top 3 with detailed analysis - -6. **Prompt Engineering (prompts/)** - - `brainstorm_prompt.py`: Creative strategy generation (temp 0.9) - - `analyze_prompt.py`: Analytical strategy selection (temp 0.3) - - Both include telemetry interpretation guides - -7. **Utilities (utils/)** - - `validators.py`: Strategy validation + telemetry analysis - - `telemetry_buffer.py`: In-memory webhook data storage - -8. **Sample Data & Tests** - - Sample enriched telemetry (10 laps) - - Sample race context (Monaco, Hamilton P4) - - Component test script - - API integration test script - ---- - -## 🎯 Key Features Implemented - -### Two-Step AI Strategy Process - -**Step 1: Brainstorming** (POST /api/strategy/brainstorm) -- Generates 20 diverse strategies -- Categories: Conservative, Standard, Aggressive, Reactive, Contingency -- High creativity (temperature 0.9) -- Validates against F1 rules (min 2 tire compounds) -- Response time target: <5 seconds - -**Step 2: Analysis** (POST /api/strategy/analyze) -- Analyzes all 20 strategies -- Selects top 3: RECOMMENDED, ALTERNATIVE, CONSERVATIVE -- Low temperature (0.3) for consistency -- Provides: - - Predicted race outcomes with probabilities - - Risk assessments - - Telemetry insights - - Engineer briefs - - Driver radio scripts - - ECU commands - - Situational context -- Response time target: <10 seconds - -### Telemetry Intelligence - -The system interprets 6 enriched metrics: -- **Aero Efficiency**: Car performance (<0.6 = problem) -- **Tire Degradation**: Wear rate (>0.85 = cliff imminent) -- **ERS Charge**: Energy availability (>0.7 = can attack) -- **Fuel Optimization**: Efficiency (<0.7 = must save) -- **Driver Consistency**: Reliability (<0.75 = risky) -- **Weather Impact**: Severity (high = flexible strategy) - -### Smart Features - -1. **Automatic Telemetry Fetching**: If not provided, fetches from enrichment service -2. **Webhook Support**: Real-time push from enrichment module -3. **Trend Analysis**: Calculates degradation rates, projects tire cliff -4. **Strategy Validation**: Ensures legal strategies per F1 rules -5. **Demo Mode**: Caches responses for consistent demos -6. **Retry Logic**: Handles Gemini API failures gracefully - ---- - -## 🔧 Integration Points - -### Upstream (HPC Enrichment Module) -``` -http://localhost:8000/enriched?limit=10 -``` -**Pull model**: AI layer fetches telemetry - -**Push model (IMPLEMENTED)**: -```bash -# In enrichment service .env: -NEXT_STAGE_CALLBACK_URL=http://localhost:9000/api/ingest/enriched -``` -Enrichment service pushes to AI layer webhook - -### Downstream (Frontend/Display) -``` -http://localhost:9000/api/strategy/brainstorm -http://localhost:9000/api/strategy/analyze -``` - ---- - -## 📊 Testing Results - -### Component Tests ✅ -``` -✓ Parsed 10 telemetry records -✓ Parsed race context for Hamilton -✓ Tire degradation rate: 0.0300 per lap -✓ Aero efficiency average: 0.840 -✓ ERS pattern: stable -✓ Projected tire cliff: Lap 33 -✓ Strategy validation working correctly -✓ Telemetry summary generation working -✓ Generated brainstorm prompt (4877 characters) -``` - -All data models, validators, and prompt generation working perfectly! - ---- - -## 🚀 How to Use - -### 1. Setup (One-time) - -```bash -cd ai_intelligence_layer - -# Already done: -# - Virtual environment created (myenv) -# - Dependencies installed -# - .env file created - -# YOU NEED TO DO: -# Add your Gemini API key to .env -nano .env -# Replace: GEMINI_API_KEY=your_gemini_api_key_here -``` - -Get a Gemini API key: https://makersuite.google.com/app/apikey - -### 2. Start the Service - -```bash -# Option 1: Direct -cd ai_intelligence_layer -source myenv/bin/activate -python main.py - -# Option 2: With uvicorn -uvicorn main:app --host 0.0.0.0 --port 9000 --reload -``` - -### 3. Test the Service - -```bash -# Quick health check -curl http://localhost:9000/api/health - -# Full integration test -./test_api.sh - -# Manual test -curl -X POST http://localhost:9000/api/strategy/brainstorm \ - -H "Content-Type: application/json" \ - -d @- << EOF -{ - "enriched_telemetry": $(cat sample_data/sample_enriched_telemetry.json), - "race_context": $(cat sample_data/sample_race_context.json) -} -EOF -``` - ---- - -## 📁 Project Structure - -``` -ai_intelligence_layer/ -├── main.py # FastAPI app ✅ -├── config.py # Settings ✅ -├── requirements.txt # Dependencies ✅ -├── .env # Configuration ✅ -├── .env.example # Template ✅ -├── README.md # Documentation ✅ -├── test_api.sh # API tests ✅ -├── test_components.py # Unit tests ✅ -│ -├── models/ -│ ├── input_models.py # Request schemas ✅ -│ ├── output_models.py # Response schemas ✅ -│ └── internal_models.py # Internal models ✅ -│ -├── services/ -│ ├── gemini_client.py # Gemini wrapper ✅ -│ ├── telemetry_client.py # Enrichment API ✅ -│ ├── strategy_generator.py # Brainstorm logic ✅ -│ └── strategy_analyzer.py # Analysis logic ✅ -│ -├── prompts/ -│ ├── brainstorm_prompt.py # Step 1 prompt ✅ -│ └── analyze_prompt.py # Step 2 prompt ✅ -│ -├── utils/ -│ ├── validators.py # Validation logic ✅ -│ └── telemetry_buffer.py # Webhook buffer ✅ -│ -└── sample_data/ - ├── sample_enriched_telemetry.json ✅ - └── sample_race_context.json ✅ -``` - -**Total Files Created: 23** -**Lines of Code: ~3,500+** - ---- - -## 🎨 Example Output - -### Brainstorm Response (20 strategies) -```json -{ - "strategies": [ - { - "strategy_id": 1, - "strategy_name": "Conservative 1-Stop", - "stop_count": 1, - "pit_laps": [35], - "tire_sequence": ["medium", "hard"], - "risk_level": "low", - ... - }, - // ... 19 more - ] -} -``` - -### Analyze Response (Top 3 with full details) -```json -{ - "top_strategies": [ - { - "rank": 1, - "classification": "RECOMMENDED", - "predicted_outcome": { - "finish_position_most_likely": 3, - "p1_probability": 8, - "p3_probability": 45, - "confidence_score": 78 - }, - "engineer_brief": { - "title": "Aggressive Undercut Lap 28", - "summary": "67% chance P3 or better", - "execution_steps": [...] - }, - "driver_audio_script": "Box this lap. Softs going on...", - "ecu_commands": { - "fuel_mode": "RICH", - "ers_strategy": "AGGRESSIVE_DEPLOY", - "engine_mode": "PUSH" - } - }, - // ... 2 more strategies - ], - "situational_context": { - "critical_decision_point": "Next 3 laps crucial", - "time_sensitivity": "Decision needed within 2 laps" - } -} -``` - ---- - -## 🏆 Innovation Highlights - -### What Makes This Special - -1. **Real HPC Integration**: Uses actual enriched telemetry from HPC simulations -2. **Dual-LLM Process**: Brainstorm diversity + analytical selection -3. **Telemetry Intelligence**: Interprets metrics to project tire cliffs, fuel needs -4. **Production-Ready**: Validation, error handling, retry logic, webhooks -5. **Race-Ready Output**: Includes driver radio scripts, ECU commands, engineer briefs -6. **F1 Rule Compliance**: Validates tire compound rules, pit window constraints - -### Technical Excellence - -- **Pydantic Models**: Full type safety and validation -- **Async/Await**: Non-blocking API calls -- **Smart Fallbacks**: Auto-fetch telemetry if not provided -- **Configurable**: Temperature, timeouts, retry logic all adjustable -- **Demo Mode**: Repeatable results for presentations -- **Comprehensive Testing**: Component tests + integration tests - ---- - -## 🐛 Known Limitations - -1. **Requires Gemini API Key**: Must configure before use -2. **Enrichment Service Dependency**: Best with localhost:8000 running -3. **Single Race Support**: Designed for one race at a time -4. **English Only**: Prompts and outputs in English - ---- - -## 🔜 Next Steps - -### To Deploy This -1. Add your Gemini API key to `.env` -2. Ensure enrichment service is running on port 8000 -3. Start this service: `python main.py` -4. Test with: `./test_api.sh` - -### To Enhance (Future) -- Multi-race session management -- Historical strategy learning -- Real-time streaming updates -- Frontend dashboard integration -- Multi-language support - ---- - -## 📞 Troubleshooting - -### "Import errors" in IDE -- This is normal - dependencies installed in `myenv` -- Run from terminal with venv activated -- Or configure IDE to use `myenv/bin/python` - -### "Enrichment service unreachable" -- Either start enrichment service on port 8000 -- Or provide telemetry data directly in requests - -### "Gemini API error" -- Check API key in `.env` -- Verify API quota: https://makersuite.google.com -- Check network connectivity - ---- - -## ✨ Summary - -You now have a **fully functional AI Intelligence Layer** that: - -✅ Receives enriched telemetry from HPC simulations -✅ Generates 20 diverse race strategies using AI -✅ Analyzes and selects top 3 with detailed rationale -✅ Provides actionable outputs (radio scripts, ECU commands) -✅ Integrates via REST API and webhooks -✅ Validates strategies against F1 rules -✅ Handles errors gracefully with retry logic -✅ Includes comprehensive documentation and tests - -**This is hackathon-ready and demo-ready!** 🏎️💨 - -Just add your Gemini API key and you're good to go! - ---- - -Built with ❤️ for the HPC + AI Race Strategy Hackathon diff --git a/ai_intelligence_layer/QUICKSTART.md b/ai_intelligence_layer/QUICKSTART.md deleted file mode 100644 index 2ab19b5..0000000 --- a/ai_intelligence_layer/QUICKSTART.md +++ /dev/null @@ -1,131 +0,0 @@ -# 🚀 Quick Start Guide - AI Intelligence Layer - -## ⚡ 60-Second Setup - -### 1. Get Gemini API Key -Visit: https://makersuite.google.com/app/apikey - -### 2. Configure -```bash -cd ai_intelligence_layer -nano .env -# Add your API key: GEMINI_API_KEY=your_key_here -``` - -### 3. Run -```bash -source myenv/bin/activate -python main.py -``` - -Service starts on: http://localhost:9000 - ---- - -## 🧪 Quick Test - -### Health Check -```bash -curl http://localhost:9000/api/health -``` - -### Full Test -```bash -./test_api.sh -``` - ---- - -## 📡 API Endpoints - -| Endpoint | Method | Purpose | -|----------|--------|---------| -| `/api/health` | GET | Health check | -| `/api/ingest/enriched` | POST | Webhook receiver | -| `/api/strategy/brainstorm` | POST | Generate 20 strategies | -| `/api/strategy/analyze` | POST | Select top 3 | - ---- - -## 🔗 Integration - -### With Enrichment Service (localhost:8000) - -**Option 1: Pull** (AI fetches) -```bash -# In enrichment service, AI will auto-fetch from: -# http://localhost:8000/enriched?limit=10 -``` - -**Option 2: Push** (Webhook - RECOMMENDED) -```bash -# In enrichment service .env: -NEXT_STAGE_CALLBACK_URL=http://localhost:9000/api/ingest/enriched -``` - ---- - -## 📦 What You Get - -### Input -- Enriched telemetry (aero, tires, ERS, fuel, consistency) -- Race context (track, position, competitors) - -### Output -- **20 diverse strategies** (conservative → aggressive) -- **Top 3 analyzed** with: - - Win probabilities - - Risk assessment - - Engineer briefs - - Driver radio scripts - - ECU commands - ---- - -## 🎯 Example Usage - -### Brainstorm -```bash -curl -X POST http://localhost:9000/api/strategy/brainstorm \ - -H "Content-Type: application/json" \ - -d '{ - "race_context": { - "race_info": {"track_name": "Monaco", "current_lap": 27, "total_laps": 58}, - "driver_state": {"driver_name": "Hamilton", "current_position": 4} - } - }' -``` - -### Analyze -```bash -curl -X POST http://localhost:9000/api/strategy/analyze \ - -H "Content-Type: application/json" \ - -d '{ - "race_context": {...}, - "strategies": [...] - }' -``` - ---- - -## 🐛 Troubleshooting - -| Issue | Solution | -|-------|----------| -| API key error | Add `GEMINI_API_KEY` to `.env` | -| Enrichment unreachable | Start enrichment service or provide telemetry data | -| Import errors | Activate venv: `source myenv/bin/activate` | - ---- - -## 📚 Documentation - -- **Full docs**: `README.md` -- **Implementation details**: `IMPLEMENTATION_SUMMARY.md` -- **Sample data**: `sample_data/` - ---- - -## ✅ Status - -All systems operational! Ready to generate race strategies! 🏎️💨 diff --git a/ai_intelligence_layer/RACE_CONTEXT.md b/ai_intelligence_layer/RACE_CONTEXT.md deleted file mode 100644 index 1e07982..0000000 --- a/ai_intelligence_layer/RACE_CONTEXT.md +++ /dev/null @@ -1,294 +0,0 @@ -# Race Context Guide - -## Why Race Context is Separate from Telemetry - -**Enrichment Service** (port 8000): -- Provides: **Enriched telemetry** (changes every lap) -- Example: tire degradation, aero efficiency, ERS charge - -**Client/Frontend**: -- Provides: **Race context** (changes less frequently) -- Example: driver name, current position, track info, competitors - -This separation is intentional: -- Telemetry changes **every lap** (real-time HPC data) -- Race context changes **occasionally** (position changes, pit stops) -- Keeps enrichment service simple and focused - -## How to Call Brainstorm with Both - -### Option 1: Client Provides Both (Recommended) - -```bash -curl -X POST http://localhost:9000/api/strategy/brainstorm \ - -H "Content-Type: application/json" \ - -d '{ - "enriched_telemetry": [ - { - "lap": 27, - "aero_efficiency": 0.85, - "tire_degradation_index": 0.72, - "ers_charge": 0.78, - "fuel_optimization_score": 0.82, - "driver_consistency": 0.88, - "weather_impact": "low" - } - ], - "race_context": { - "race_info": { - "track_name": "Monaco", - "current_lap": 27, - "total_laps": 58, - "weather_condition": "Dry", - "track_temp_celsius": 42 - }, - "driver_state": { - "driver_name": "Hamilton", - "current_position": 4, - "current_tire_compound": "medium", - "tire_age_laps": 14, - "fuel_remaining_percent": 47 - }, - "competitors": [] - } - }' -``` - -### Option 2: AI Layer Fetches Telemetry, Client Provides Context - -```bash -# Enrichment service POSTs telemetry to webhook -# Then client calls: - -curl -X POST http://localhost:9000/api/strategy/brainstorm \ - -H "Content-Type: application/json" \ - -d '{ - "race_context": { - "race_info": {...}, - "driver_state": {...}, - "competitors": [] - } - }' -``` - -AI layer will use telemetry from: -1. **Buffer** (if webhook has pushed data) ← CURRENT SETUP -2. **GET /enriched** from enrichment service (fallback) - -## Creating a Race Context Template - -Here's a reusable template: - -```json -{ - "race_context": { - "race_info": { - "track_name": "Monaco", - "current_lap": 27, - "total_laps": 58, - "weather_condition": "Dry", - "track_temp_celsius": 42 - }, - "driver_state": { - "driver_name": "Hamilton", - "current_position": 4, - "current_tire_compound": "medium", - "tire_age_laps": 14, - "fuel_remaining_percent": 47 - }, - "competitors": [ - { - "position": 1, - "driver": "Verstappen", - "tire_compound": "hard", - "tire_age_laps": 18, - "gap_seconds": -12.5 - }, - { - "position": 2, - "driver": "Leclerc", - "tire_compound": "medium", - "tire_age_laps": 10, - "gap_seconds": -5.2 - }, - { - "position": 3, - "driver": "Norris", - "tire_compound": "medium", - "tire_age_laps": 12, - "gap_seconds": -2.1 - }, - { - "position": 5, - "driver": "Sainz", - "tire_compound": "soft", - "tire_age_laps": 5, - "gap_seconds": 3.8 - } - ] - } -} -``` - -## Where Does Race Context Come From? - -In a real system, race context typically comes from: - -1. **Timing System** - Official F1 timing data - - Current positions - - Gap times - - Lap numbers - -2. **Team Database** - Historical race data - - Track information - - Total laps for this race - - Weather forecasts - -3. **Pit Wall** - Live observations - - Competitor tire strategies - - Weather conditions - - Track temperature - -4. **Telemetry Feed** - Some data overlaps - - Driver's current tires - - Tire age - - Fuel remaining - -## Recommended Architecture - -``` -┌─────────────────────┐ -│ Timing System │ -│ (Race Control) │ -└──────────┬──────────┘ - │ - ▼ -┌─────────────────────┐ ┌─────────────────────┐ -│ Frontend/Client │ │ Enrichment Service │ -│ │ │ (Port 8000) │ -│ Manages: │ │ │ -│ - Race context │ │ Manages: │ -│ - UI state │ │ - Telemetry │ -│ - User inputs │ │ - HPC enrichment │ -└──────────┬──────────┘ └──────────┬──────────┘ - │ │ - │ │ POST /ingest/enriched - │ │ (telemetry only) - │ ▼ - │ ┌─────────────────────┐ - │ │ AI Layer Buffer │ - │ │ (telemetry only) │ - │ └─────────────────────┘ - │ │ - │ POST /api/strategy/brainstorm │ - │ (race_context + telemetry) │ - └───────────────────────────────┤ - │ - ▼ - ┌─────────────────────┐ - │ AI Strategy Layer │ - │ (Port 9000) │ - │ │ - │ Generates 3 │ - │ strategies │ - └─────────────────────┘ -``` - -## Python Example: Calling with Race Context - -```python -import httpx - -async def get_race_strategies(race_context: dict): - """ - Get strategies from AI layer. - - Args: - race_context: Current race state - - Returns: - 3 strategies with pit plans and risk assessments - """ - url = "http://localhost:9000/api/strategy/brainstorm" - - payload = { - "race_context": race_context - # enriched_telemetry is optional - AI will use buffer or fetch - } - - async with httpx.AsyncClient(timeout=60.0) as client: - response = await client.post(url, json=payload) - response.raise_for_status() - return response.json() - -# Usage: -race_context = { - "race_info": { - "track_name": "Monaco", - "current_lap": 27, - "total_laps": 58, - "weather_condition": "Dry", - "track_temp_celsius": 42 - }, - "driver_state": { - "driver_name": "Hamilton", - "current_position": 4, - "current_tire_compound": "medium", - "tire_age_laps": 14, - "fuel_remaining_percent": 47 - }, - "competitors": [] -} - -strategies = await get_race_strategies(race_context) -print(f"Generated {len(strategies['strategies'])} strategies") -``` - -## Alternative: Enrichment Service Sends Full Payload - -If you really want enrichment service to send race context too, you'd need to: - -### 1. Store race context in enrichment service - -```python -# In hpcsim/api.py -_race_context = { - "race_info": {...}, - "driver_state": {...}, - "competitors": [] -} - -@app.post("/set_race_context") -async def set_race_context(context: Dict[str, Any]): - """Update race context (call this when race state changes).""" - global _race_context - _race_context = context - return {"status": "ok"} -``` - -### 2. Send both in webhook - -```python -# In ingest_telemetry endpoint -if _CALLBACK_URL: - payload = { - "enriched_telemetry": [enriched], - "race_context": _race_context - } - await client.post(_CALLBACK_URL, json=payload) -``` - -### 3. Update AI webhook to handle full payload - -But this adds complexity. **I recommend keeping it simple**: client provides race_context when calling brainstorm. - ---- - -## Current Working Setup - -✅ **Enrichment service** → POSTs telemetry to `/api/ingest/enriched` -✅ **AI layer** → Stores telemetry in buffer -✅ **Client** → Calls `/api/strategy/brainstorm` with race_context -✅ **AI layer** → Uses buffer telemetry + provided race_context → Generates strategies - -This is clean, simple, and follows single responsibility principle! diff --git a/ai_intelligence_layer/RUN_SERVICES.md b/ai_intelligence_layer/RUN_SERVICES.md deleted file mode 100644 index 05d2d86..0000000 --- a/ai_intelligence_layer/RUN_SERVICES.md +++ /dev/null @@ -1,290 +0,0 @@ -# 🚀 Quick Start: Full System Test - -## Overview - -Test the complete webhook integration flow: -1. **Enrichment Service** (port 8000) - Receives telemetry, enriches it, POSTs to AI layer -2. **AI Intelligence Layer** (port 9000) - Receives enriched data, generates 3 strategies - -## Step-by-Step Testing - -### 1. Start the Enrichment Service (Port 8000) - -From the **project root** (`HPCSimSite/`): - -```bash -# Option A: Using the serve script -python3 scripts/serve.py -``` - -**Or from any directory:** - -```bash -cd /Users/rishubmadhav/Documents/GitHub/HPCSimSite -python3 -m uvicorn hpcsim.api:app --host 0.0.0.0 --port 8000 -``` - -You should see: -``` -INFO: Uvicorn running on http://0.0.0.0:8000 -INFO: Application startup complete. -``` - -**Verify it's running:** -```bash -curl http://localhost:8000/healthz -# Should return: {"status":"ok","stored":0} -``` - -### 2. Configure Webhook Callback - -The enrichment service needs to know where to send enriched data. - -**Option A: Set environment variable (before starting)** -```bash -export NEXT_STAGE_CALLBACK_URL=http://localhost:9000/api/ingest/enriched -python3 scripts/serve.py -``` - -**Option B: For testing, manually POST enriched data** - -You can skip the callback and use `test_webhook_push.py` to simulate it (already working!). - -### 3. Start the AI Intelligence Layer (Port 9000) - -In a **new terminal**, from `ai_intelligence_layer/`: - -```bash -cd /Users/rishubmadhav/Documents/GitHub/HPCSimSite/ai_intelligence_layer -source myenv/bin/activate # Activate virtual environment -python main.py -``` - -You should see: -``` -INFO - Starting AI Intelligence Layer on port 9000 -INFO - Strategy count: 3 -INFO - All services initialized successfully -INFO: Uvicorn running on http://0.0.0.0:9000 -``` - -**Verify it's running:** -```bash -curl http://localhost:9000/api/health -``` - -### 4. Test the Webhook Flow - -**Method 1: Simulate enrichment service (fastest)** - -```bash -cd ai_intelligence_layer -python3 test_webhook_push.py --loop 5 -``` - -Output: -``` -✓ Posted lap 27 - Buffer size: 1 records -✓ Posted lap 28 - Buffer size: 2 records -... -Posted 5/5 records successfully -``` - -**Method 2: POST to enrichment service (full integration)** - -POST raw telemetry to enrichment service, it will enrich and forward: - -```bash -curl -X POST http://localhost:8000/ingest/telemetry \ - -H "Content-Type: application/json" \ - -d '{ - "lap": 27, - "speed": 310, - "tire_temp": 95, - "fuel_level": 45 - }' -``` - -*Note: This requires NEXT_STAGE_CALLBACK_URL to be set* - -### 5. Generate Strategies - -```bash -cd ai_intelligence_layer -python3 test_buffer_usage.py -``` - -Output: -``` -Testing FAST brainstorm with buffered telemetry... -(Configured for 3 strategies - fast and diverse!) - -✓ Brainstorm succeeded! - Generated 3 strategies - Saved to: /tmp/brainstorm_strategies.json - - Strategies: - 1. Conservative Stay Out (1-stop, low risk) - Tires: medium → hard - Pits at: laps [35] - Extend current stint then hard tires to end - - 2. Standard Undercut (1-stop, medium risk) - Tires: medium → hard - Pits at: laps [32] - Pit before tire cliff for track position - - 3. Aggressive Two-Stop (2-stop, high risk) - Tires: medium → soft → hard - Pits at: laps [30, 45] - Early pit for fresh rubber and pace advantage - -✓ SUCCESS: AI layer is using webhook buffer! - Full JSON saved to /tmp/brainstorm_strategies.json -``` - -### 6. View the Results - -```bash -cat /tmp/brainstorm_strategies.json | python3 -m json.tool -``` - -Or just: -```bash -cat /tmp/brainstorm_strategies.json -``` - -## Terminal Setup - -Here's the recommended terminal layout: - -``` -┌─────────────────────────┬─────────────────────────┐ -│ Terminal 1 │ Terminal 2 │ -│ Enrichment Service │ AI Intelligence Layer │ -│ (Port 8000) │ (Port 9000) │ -│ │ │ -│ $ cd HPCSimSite │ $ cd ai_intelligence... │ -│ $ python3 scripts/ │ $ source myenv/bin/... │ -│ serve.py │ $ python main.py │ -│ │ │ -│ Running... │ Running... │ -└─────────────────────────┴─────────────────────────┘ -┌───────────────────────────────────────────────────┐ -│ Terminal 3 - Testing │ -│ │ -│ $ cd ai_intelligence_layer │ -│ $ python3 test_webhook_push.py --loop 5 │ -│ $ python3 test_buffer_usage.py │ -└───────────────────────────────────────────────────┘ -``` - -## Current Configuration - -### Enrichment Service (Port 8000) -- **Endpoints:** - - `POST /ingest/telemetry` - Receive raw telemetry - - `POST /enriched` - Manually post enriched data - - `GET /enriched?limit=N` - Retrieve recent enriched records - - `GET /healthz` - Health check - -### AI Intelligence Layer (Port 9000) -- **Endpoints:** - - `GET /api/health` - Health check - - `POST /api/ingest/enriched` - Webhook receiver (enrichment service POSTs here) - - `POST /api/strategy/brainstorm` - Generate 3 strategies - - ~~`POST /api/strategy/analyze`~~ - **DISABLED** for speed - -- **Configuration:** - - `STRATEGY_COUNT=3` - Generates 3 strategies - - `FAST_MODE=true` - Uses shorter prompts - - Response time: ~15-20 seconds (was ~2 minutes with 20 strategies + analysis) - -## Troubleshooting - -### Enrichment service won't start -```bash -# Check if port 8000 is already in use -lsof -i :8000 - -# Kill existing process -kill -9 - -# Or use a different port -python3 -m uvicorn hpcsim.api:app --host 0.0.0.0 --port 8001 -``` - -### AI layer can't find enrichment service -If you see: `"Cannot connect to enrichment service at http://localhost:8000"` - -**Solution:** The buffer is empty and it's trying to pull from enrichment service. - -```bash -# Push some data via webhook first: -python3 test_webhook_push.py --loop 5 -``` - -### Virtual environment issues -```bash -cd ai_intelligence_layer - -# Check if venv exists -ls -la myenv/ - -# If missing, recreate: -python3 -m venv myenv -source myenv/bin/activate -pip install -r requirements.txt -``` - -### Module not found errors -```bash -# For enrichment service -cd /Users/rishubmadhav/Documents/GitHub/HPCSimSite -export PYTHONPATH=$PWD:$PYTHONPATH -python3 scripts/serve.py - -# For AI layer -cd ai_intelligence_layer -source myenv/bin/activate -python main.py -``` - -## Full Integration Test Workflow - -```bash -# Terminal 1: Start enrichment -cd /Users/rishubmadhav/Documents/GitHub/HPCSimSite -export NEXT_STAGE_CALLBACK_URL=http://localhost:9000/api/ingest/enriched -python3 scripts/serve.py - -# Terminal 2: Start AI layer -cd /Users/rishubmadhav/Documents/GitHub/HPCSimSite/ai_intelligence_layer -source myenv/bin/activate -python main.py - -# Terminal 3: Test webhook push -cd /Users/rishubmadhav/Documents/GitHub/HPCSimSite/ai_intelligence_layer -python3 test_webhook_push.py --loop 5 - -# Terminal 3: Generate strategies -python3 test_buffer_usage.py - -# View results -cat /tmp/brainstorm_strategies.json | python3 -m json.tool -``` - -## What's Next? - -1. ✅ **Both services running** - Enrichment on 8000, AI on 9000 -2. ✅ **Webhook tested** - Data flows from enrichment → AI layer -3. ✅ **Strategies generated** - 3 strategies in ~20 seconds -4. ⏭️ **Real telemetry** - Connect actual race data source -5. ⏭️ **Frontend** - Build UI to display strategies -6. ⏭️ **Production** - Increase to 20 strategies, enable analysis - ---- - -**Status:** 🚀 Both services ready to run! -**Performance:** ~20 seconds for 3 strategies (vs 2+ minutes for 20 + analysis) -**Integration:** Webhook push working perfectly diff --git a/ai_intelligence_layer/STATUS.md b/ai_intelligence_layer/STATUS.md deleted file mode 100644 index c1f24d9..0000000 --- a/ai_intelligence_layer/STATUS.md +++ /dev/null @@ -1,236 +0,0 @@ -# ✅ AI Intelligence Layer - WORKING! - -## 🎉 Success Summary - -The AI Intelligence Layer is now **fully functional** and has been successfully tested! - -### Test Results from Latest Run: - -``` -✓ Health Check: PASSED (200 OK) -✓ Brainstorm: PASSED (200 OK) - - Generated 19/20 strategies in 48 seconds - - 1 strategy filtered (didn't meet F1 tire compound rule) - - Fast mode working perfectly -✓ Service: RUNNING (port 9000) -``` - -## 📊 Performance Metrics - -| Metric | Target | Actual | Status | -|--------|--------|--------|--------| -| Health check | <1s | <1s | ✅ | -| Brainstorm | 15-30s | 48s | ⚠️ Acceptable | -| Service uptime | Stable | Stable | ✅ | -| Fast mode | Enabled | Enabled | ✅ | - -**Note:** 48s is slightly slower than the 15-30s target, but well within acceptable range. The Gemini API response time varies based on load. - -## 🚀 How to Use - -### 1. Start the Service -```bash -cd ai_intelligence_layer -source myenv/bin/activate -python main.py -``` - -### 2. Run Tests - -**Best option - Python test script:** -```bash -python3 test_api.py -``` - -**Alternative - Shell script:** -```bash -./test_api.sh -``` - -### 3. Check Results -```bash -# View generated strategies -cat /tmp/brainstorm_result.json | python3 -m json.tool | head -50 - -# View analysis results -cat /tmp/analyze_result.json | python3 -m json.tool | head -100 -``` - -## ✨ What's Working - -### ✅ Core Features -- [x] FastAPI service on port 9000 -- [x] Health check endpoint -- [x] Webhook receiver for enrichment data -- [x] Strategy brainstorming (20 diverse strategies) -- [x] Strategy analysis (top 3 selection) -- [x] Automatic telemetry fetching from enrichment service -- [x] F1 rule validation (tire compounds) -- [x] Fast mode for quicker responses -- [x] Retry logic with exponential backoff -- [x] Comprehensive error handling - -### ✅ AI Features -- [x] Gemini 2.5 Flash integration -- [x] JSON response parsing -- [x] Prompt optimization (fast mode) -- [x] Strategy diversity (5 types) -- [x] Risk assessment -- [x] Telemetry interpretation -- [x] Tire cliff projection -- [x] Detailed analysis outputs - -### ✅ Output Quality -- [x] Win probability predictions -- [x] Risk assessments -- [x] Engineer briefs -- [x] Driver radio scripts -- [x] ECU commands (fuel, ERS, engine modes) -- [x] Situational context - -## 📝 Configuration - -Current optimal settings in `.env`: -```bash -GEMINI_MODEL=gemini-2.5-flash # Fast, good quality -FAST_MODE=true # Optimized prompts -BRAINSTORM_TIMEOUT=90 # Sufficient time -ANALYZE_TIMEOUT=120 # Sufficient time -DEMO_MODE=false # Real-time mode -``` - -## 🎯 Next Steps - -### For Demo/Testing: -1. ✅ Service is ready to use -2. ✅ Test scripts available -3. ⏭️ Try with different race scenarios -4. ⏭️ Test webhook integration with enrichment service - -### For Production: -1. ⏭️ Set up monitoring/logging -2. ⏭️ Add rate limiting -3. ⏭️ Consider caching frequently requested strategies -4. ⏭️ Add authentication if exposing publicly - -### Optional Enhancements: -1. ⏭️ Frontend dashboard -2. ⏭️ Real-time strategy updates during race -3. ⏭️ Historical strategy learning -4. ⏭️ Multi-driver support - -## 🔧 Troubleshooting Guide - -### Issue: "Connection refused" -**Solution:** Start the service -```bash -python main.py -``` - -### Issue: Slow responses (>60s) -**Solution:** Already fixed with: -- Fast mode enabled -- Increased timeouts -- Optimized prompts - -### Issue: "422 Unprocessable Content" -**Solution:** Use `test_api.py` instead of `test_api.sh` -- Python script handles JSON properly -- No external dependencies - -### Issue: Service crashes -**Solution:** Check logs -```bash -python main.py 2>&1 | tee ai_service.log -``` - -## 📚 Documentation - -| File | Purpose | -|------|---------| -| `README.md` | Full documentation | -| `QUICKSTART.md` | 60-second setup | -| `TESTING.md` | Testing guide | -| `TIMEOUT_FIX.md` | Timeout resolution details | -| `ARCHITECTURE.md` | System architecture | -| `IMPLEMENTATION_SUMMARY.md` | Technical details | - -## 🎓 Example Usage - -### Manual API Call -```python -import requests - -# Brainstorm -response = requests.post('http://localhost:9000/api/strategy/brainstorm', json={ - "race_context": { - "race_info": { - "track_name": "Monaco", - "current_lap": 27, - "total_laps": 58, - "weather_condition": "Dry", - "track_temp_celsius": 42 - }, - "driver_state": { - "driver_name": "Hamilton", - "current_position": 4, - "current_tire_compound": "medium", - "tire_age_laps": 14, - "fuel_remaining_percent": 47 - }, - "competitors": [...] - } -}) - -strategies = response.json()['strategies'] -print(f"Generated {len(strategies)} strategies") -``` - -## 🌟 Key Achievements - -1. **Built from scratch** - Complete FastAPI application with AI integration -2. **Production-ready** - Error handling, validation, retry logic -3. **Well-documented** - 7 documentation files, inline comments -4. **Tested** - Component tests + integration tests passing -5. **Optimized** - Fast mode reduces response time significantly -6. **Flexible** - Webhook + polling support for enrichment data -7. **Smart** - Interprets telemetry, projects tire cliffs, validates F1 rules -8. **Complete** - All requirements from original spec implemented - -## 📊 Files Created - -- **Core:** 7 files (main, config, models) -- **Services:** 4 files (Gemini, telemetry, strategy generation/analysis) -- **Prompts:** 2 files (brainstorm, analyze) -- **Utils:** 2 files (validators, buffer) -- **Tests:** 3 files (component, API shell, API Python) -- **Docs:** 7 files (README, quickstart, testing, timeout fix, architecture, implementation, this file) -- **Config:** 3 files (.env, .env.example, requirements.txt) -- **Sample Data:** 2 files (telemetry, race context) - -**Total: 30+ files, ~4,000+ lines of code** - -## 🏁 Final Status - -``` -╔═══════════════════════════════════════════════╗ -║ AI INTELLIGENCE LAYER - FULLY OPERATIONAL ║ -║ ║ -║ ✅ Service Running ║ -║ ✅ Tests Passing ║ -║ ✅ Fast Mode Working ║ -║ ✅ Gemini Integration Working ║ -║ ✅ Strategy Generation Working ║ -║ ✅ Documentation Complete ║ -║ ║ -║ READY FOR HACKATHON! 🏎️💨 ║ -╚═══════════════════════════════════════════════╝ -``` - ---- - -**Built with ❤️ for the HPC + AI Race Strategy Hackathon** - -Last updated: October 18, 2025 -Version: 1.0.0 -Status: ✅ Production Ready diff --git a/ai_intelligence_layer/TESTING.md b/ai_intelligence_layer/TESTING.md deleted file mode 100644 index 9fd1f1a..0000000 --- a/ai_intelligence_layer/TESTING.md +++ /dev/null @@ -1,219 +0,0 @@ -# Testing the AI Intelligence Layer - -## Quick Test Options - -### Option 1: Python Script (RECOMMENDED - No dependencies) -```bash -python3 test_api.py -``` - -**Advantages:** -- ✅ No external tools required -- ✅ Clear, formatted output -- ✅ Built-in error handling -- ✅ Works on all systems - -### Option 2: Shell Script -```bash -./test_api.sh -``` - -**Note:** Uses pure Python for JSON processing (no `jq` required) - -### Option 3: Manual Testing - -#### Health Check -```bash -curl http://localhost:9000/api/health | python3 -m json.tool -``` - -#### Brainstorm Test -```bash -python3 << 'EOF' -import json -import urllib.request - -# Load data -with open('sample_data/sample_enriched_telemetry.json') as f: - telemetry = json.load(f) -with open('sample_data/sample_race_context.json') as f: - context = json.load(f) - -# Make request -data = json.dumps({ - "enriched_telemetry": telemetry, - "race_context": context -}).encode('utf-8') - -req = urllib.request.Request( - 'http://localhost:9000/api/strategy/brainstorm', - data=data, - headers={'Content-Type': 'application/json'} -) - -with urllib.request.urlopen(req, timeout=120) as response: - result = json.loads(response.read()) - print(f"Generated {len(result['strategies'])} strategies") - for s in result['strategies'][:3]: - print(f"{s['strategy_id']}. {s['strategy_name']} - {s['risk_level']} risk") -EOF -``` - -## Expected Output - -### Successful Test Run - -``` -====================================================================== -AI Intelligence Layer - Test Suite -====================================================================== -1. Testing health endpoint... - ✓ Status: healthy - ✓ Service: AI Intelligence Layer - ✓ Demo mode: False - -2. Testing brainstorm endpoint... - (This may take 15-30 seconds...) - ✓ Generated 20 strategies in 18.3s - - Sample strategies: - 1. Conservative 1-Stop - Stops: 1, Risk: low - 2. Standard Medium-Hard - Stops: 1, Risk: medium - 3. Aggressive Undercut - Stops: 2, Risk: high - -3. Testing analyze endpoint... - (This may take 20-40 seconds...) - ✓ Analysis complete in 24.7s - - Top 3 strategies: - - 1. Aggressive Undercut (RECOMMENDED) - Predicted: P3 - P3 or better: 75% - Risk: medium - - 2. Standard Two-Stop (ALTERNATIVE) - Predicted: P4 - P3 or better: 63% - Risk: medium - - 3. Conservative 1-Stop (CONSERVATIVE) - Predicted: P5 - P3 or better: 37% - Risk: low - -====================================================================== -RECOMMENDED STRATEGY DETAILS: -====================================================================== - -Engineer Brief: - Undercut Leclerc on lap 32. 75% chance of P3 or better. - -Driver Radio: - "Box this lap. Soft tires going on. Push mode for next 8 laps." - -ECU Commands: - Fuel: RICH - ERS: AGGRESSIVE_DEPLOY - Engine: PUSH - -====================================================================== - -====================================================================== -✓ ALL TESTS PASSED! -====================================================================== - -Results saved to: - - /tmp/brainstorm_result.json - - /tmp/analyze_result.json -``` - -## Troubleshooting - -### "Connection refused" -```bash -# Service not running. Start it: -python main.py -``` - -### "Timeout" errors -```bash -# Check .env settings: -cat .env | grep TIMEOUT - -# Should see: -# BRAINSTORM_TIMEOUT=90 -# ANALYZE_TIMEOUT=120 - -# Also check Fast Mode is enabled: -cat .env | grep FAST_MODE -# Should see: FAST_MODE=true -``` - -### "422 Unprocessable Content" -This usually means invalid JSON in the request. The new test scripts handle this automatically. - -### Test takes too long -```bash -# Enable fast mode in .env: -FAST_MODE=true - -# Restart service: -# Press Ctrl+C in the terminal running python main.py -# Then: python main.py -``` - -## Performance Benchmarks - -With `FAST_MODE=true` and `gemini-2.5-flash`: - -| Test | Expected Time | Status | -|------|--------------|--------| -| Health | <1s | ✅ | -| Brainstorm | 15-30s | ✅ | -| Analyze | 20-40s | ✅ | -| **Total** | **40-70s** | ✅ | - -## Component Tests - -To test just the data models and validators (no API calls): - -```bash -python test_components.py -``` - -This runs instantly and doesn't require the Gemini API. - -## Files Created During Tests - -- `/tmp/test_request.json` - Brainstorm request payload -- `/tmp/brainstorm_result.json` - 20 generated strategies -- `/tmp/analyze_request.json` - Analyze request payload -- `/tmp/analyze_result.json` - Top 3 analyzed strategies - -You can inspect these files to see the full API responses. - -## Integration with Enrichment Service - -If the enrichment service is running on `localhost:8000`, the AI layer will automatically fetch telemetry data when not provided in the request: - -```bash -# Test without providing telemetry (will fetch from enrichment service) -curl -X POST http://localhost:9000/api/strategy/brainstorm \ - -H "Content-Type: application/json" \ - -d '{ - "race_context": { - "race_info": {"track_name": "Monaco", "current_lap": 27, "total_laps": 58}, - "driver_state": {"driver_name": "Hamilton", "current_position": 4} - } - }' -``` - ---- - -**Ready to test!** 🚀 - -Just run: `python3 test_api.py` diff --git a/ai_intelligence_layer/TIMEOUT_FIX.md b/ai_intelligence_layer/TIMEOUT_FIX.md deleted file mode 100644 index 791e2cb..0000000 --- a/ai_intelligence_layer/TIMEOUT_FIX.md +++ /dev/null @@ -1,179 +0,0 @@ -# Timeout Fix Guide - -## Problem -Gemini API timing out with 504 errors after ~30 seconds. - -## Solution Applied ✅ - -### 1. Increased Timeouts -**File: `.env`** -```bash -BRAINSTORM_TIMEOUT=90 # Increased from 30s -ANALYZE_TIMEOUT=120 # Increased from 60s -``` - -### 2. Added Fast Mode -**File: `.env`** -```bash -FAST_MODE=true # Use shorter, optimized prompts -``` - -Fast mode reduces prompt length by ~60% while maintaining quality: -- Brainstorm: ~4900 chars → ~1200 chars -- Analyze: ~6500 chars → ~1800 chars - -### 3. Improved Retry Logic -**File: `services/gemini_client.py`** -- Longer backoff for timeout errors (5s instead of 2s) -- Minimum timeout of 60s for API calls -- Better error detection - -### 4. Model Selection -You're using `gemini-2.5-flash` which is good! It's: -- ✅ Faster than Pro -- ✅ Cheaper -- ✅ Good quality for this use case - -## How to Use - -### Option 1: Fast Mode (RECOMMENDED for demos) -```bash -# In .env -FAST_MODE=true -``` -- Faster responses (~10-20s per call) -- Shorter prompts -- Still high quality - -### Option 2: Full Mode (for production) -```bash -# In .env -FAST_MODE=false -``` -- More detailed prompts -- Slightly better quality -- Slower (~30-60s per call) - -## Testing - -### Quick Test -```bash -# Check health -curl http://localhost:9000/api/health - -# Test with sample data (fast mode) -curl -X POST http://localhost:9000/api/strategy/brainstorm \ - -H "Content-Type: application/json" \ - -d @- << EOF -{ - "enriched_telemetry": $(cat sample_data/sample_enriched_telemetry.json), - "race_context": $(cat sample_data/sample_race_context.json) -} -EOF -``` - -## Troubleshooting - -### Still getting timeouts? - -**1. Check API quota** -- Visit: https://aistudio.google.com/apikey -- Check rate limits and quota -- Free tier: 15 requests/min, 1M tokens/min - -**2. Try different model** -```bash -# In .env, try: -GEMINI_MODEL=gemini-1.5-flash # Fastest -# or -GEMINI_MODEL=gemini-1.5-pro # Better quality, slower -``` - -**3. Increase timeouts further** -```bash -# In .env -BRAINSTORM_TIMEOUT=180 -ANALYZE_TIMEOUT=240 -``` - -**4. Reduce strategy count** -If still timing out, you can modify the code to generate fewer strategies: -- Edit `prompts/brainstorm_prompt.py` -- Change "Generate 20 strategies" to "Generate 10 strategies" - -### Network issues? - -**Check connectivity:** -```bash -# Test Google AI endpoint -curl -I https://generativelanguage.googleapis.com - -# Check if behind proxy -echo $HTTP_PROXY -echo $HTTPS_PROXY -``` - -**Use VPN if needed** - Some regions have restricted access to Google AI APIs - -### Monitor performance - -**Watch logs:** -```bash -# Start server with logs -python main.py 2>&1 | tee ai_layer.log - -# In another terminal, watch for timeouts -tail -f ai_layer.log | grep -i timeout -``` - -## Performance Benchmarks - -### Fast Mode (FAST_MODE=true) -- Brainstorm: ~15-25s -- Analyze: ~20-35s -- Total workflow: ~40-60s - -### Full Mode (FAST_MODE=false) -- Brainstorm: ~30-50s -- Analyze: ~40-70s -- Total workflow: ~70-120s - -## What Changed - -### Before -``` -Prompt: 4877 chars -Timeout: 30s -Result: ❌ 504 timeout errors -``` - -### After (Fast Mode) -``` -Prompt: ~1200 chars (75% reduction) -Timeout: 90s -Result: ✅ Works reliably -``` - -## Configuration Summary - -Your current setup: -```bash -GEMINI_MODEL=gemini-2.5-flash # Fast model -FAST_MODE=true # Optimized prompts -BRAINSTORM_TIMEOUT=90 # 3x increase -ANALYZE_TIMEOUT=120 # 2x increase -``` - -This should work reliably now! 🎉 - -## Additional Tips - -1. **For demos**: Keep FAST_MODE=true -2. **For production**: Test with FAST_MODE=false, adjust timeouts as needed -3. **Monitor quota**: Check usage at https://aistudio.google.com -4. **Cache responses**: Enable DEMO_MODE=true for repeatable demos - ---- - -**Status**: FIXED ✅ -**Ready to test**: YES 🚀 diff --git a/ai_intelligence_layer/WEBHOOK_INTEGRATION.md b/ai_intelligence_layer/WEBHOOK_INTEGRATION.md deleted file mode 100644 index 5961ad6..0000000 --- a/ai_intelligence_layer/WEBHOOK_INTEGRATION.md +++ /dev/null @@ -1,316 +0,0 @@ -# Webhook Push Integration Guide - -## Overview - -The AI Intelligence Layer supports **two integration models** for receiving enriched telemetry: - -1. **Push Model (Webhook)** - Enrichment service POSTs data to AI layer ✅ **RECOMMENDED** -2. **Pull Model** - AI layer fetches data from enrichment service (fallback) - -## Push Model (Webhook) - How It Works - -``` -┌─────────────────────┐ ┌─────────────────────┐ -│ HPC Enrichment │ POST │ AI Intelligence │ -│ Service │────────▶│ Layer │ -│ (Port 8000) │ │ (Port 9000) │ -└─────────────────────┘ └─────────────────────┘ - │ - ▼ - ┌──────────────┐ - │ Telemetry │ - │ Buffer │ - │ (in-memory) │ - └──────────────┘ - │ - ▼ - ┌──────────────┐ - │ Brainstorm │ - │ & Analyze │ - │ (Gemini AI) │ - └──────────────┘ -``` - -### Configuration - -In your **enrichment service** (port 8000), set the callback URL: - -```bash -export NEXT_STAGE_CALLBACK_URL=http://localhost:9000/api/ingest/enriched -``` - -When enrichment is complete for each lap, the service will POST to this endpoint. - -### Webhook Endpoint - -**Endpoint:** `POST /api/ingest/enriched` - -**Request Body:** Single enriched telemetry record (JSON) - -```json -{ - "lap": 27, - "lap_time_seconds": 78.456, - "tire_degradation_index": 0.72, - "fuel_remaining_kg": 45.2, - "aero_efficiency": 0.85, - "ers_recovery_rate": 0.78, - "brake_wear_index": 0.65, - "fuel_optimization_score": 0.82, - "driver_consistency": 0.88, - "predicted_tire_cliff_lap": 35, - "weather_impact": "minimal", - "hpc_simulation_id": "sim_monaco_lap27_001", - "metadata": { - "simulation_timestamp": "2025-10-18T22:15:30Z", - "confidence_level": 0.92, - "cluster_nodes_used": 8 - } -} -``` - -**Response:** - -```json -{ - "status": "received", - "lap": 27, - "buffer_size": 15 -} -``` - -### Buffer Behavior - -- **Max Size:** 100 records (configurable) -- **Storage:** In-memory (cleared on restart) -- **Retrieval:** FIFO - newest data returned first -- **Auto-cleanup:** Oldest records dropped when buffer is full - -## Testing the Webhook - -### 1. Start the AI Intelligence Layer - -```bash -cd ai_intelligence_layer -source myenv/bin/activate # or your venv -python main.py -``` - -Verify it's running: -```bash -curl http://localhost:9000/api/health -``` - -### 2. Simulate Enrichment Service Pushing Data - -**Option A: Using the test script** - -```bash -# Post single telemetry record -python3 test_webhook_push.py - -# Post 10 records with 2s delay between each -python3 test_webhook_push.py --loop 10 --delay 2 - -# Post 5 records with 1s delay -python3 test_webhook_push.py --loop 5 --delay 1 -``` - -**Option B: Using curl** - -```bash -curl -X POST http://localhost:9000/api/ingest/enriched \ - -H "Content-Type: application/json" \ - -d '{ - "lap": 27, - "lap_time_seconds": 78.456, - "tire_degradation_index": 0.72, - "fuel_remaining_kg": 45.2, - "aero_efficiency": 0.85, - "ers_recovery_rate": 0.78, - "brake_wear_index": 0.65, - "fuel_optimization_score": 0.82, - "driver_consistency": 0.88, - "predicted_tire_cliff_lap": 35, - "weather_impact": "minimal", - "hpc_simulation_id": "sim_monaco_lap27_001", - "metadata": { - "simulation_timestamp": "2025-10-18T22:15:30Z", - "confidence_level": 0.92, - "cluster_nodes_used": 8 - } - }' -``` - -### 3. Verify Buffer Contains Data - -Check the logs - you should see: -``` -INFO - Received enriched telemetry webhook: lap 27 -INFO - Added telemetry for lap 27 (buffer size: 1) -``` - -### 4. Test Strategy Generation Using Buffered Data - -**Brainstorm endpoint** (no telemetry in request = uses buffer): - -```bash -curl -X POST http://localhost:9000/api/strategy/brainstorm \ - -H "Content-Type: application/json" \ - -d '{ - "race_context": { - "race_info": { - "track_name": "Monaco", - "current_lap": 27, - "total_laps": 58, - "weather_condition": "Dry", - "track_temp_celsius": 42 - }, - "driver_state": { - "driver_name": "Hamilton", - "current_position": 4, - "current_tire_compound": "medium", - "tire_age_laps": 14, - "fuel_remaining_percent": 47 - }, - "competitors": [] - } - }' | python3 -m json.tool -``` - -Check logs for: -``` -INFO - Using 10 telemetry records from webhook buffer -``` - -## Pull Model (Fallback) - -If the buffer is empty and no telemetry is provided in the request, the AI layer will **automatically fetch** from the enrichment service: - -```bash -GET http://localhost:8000/enriched?limit=10 -``` - -This ensures the system works even without webhook configuration. - -## Priority Order - -When brainstorm/analyze endpoints are called: - -1. **Check request body** - Use `enriched_telemetry` if provided -2. **Check buffer** - Use webhook buffer if it has data -3. **Fetch from service** - Pull from enrichment service as fallback -4. **Error** - If all fail, return 400 error - -## Production Recommendations - -### For Enrichment Service - -```bash -# Configure callback URL -export NEXT_STAGE_CALLBACK_URL=http://ai-layer:9000/api/ingest/enriched - -# Add retry logic (recommended) -export CALLBACK_MAX_RETRIES=3 -export CALLBACK_TIMEOUT=10 -``` - -### For AI Layer - -```python -# config.py - Increase buffer size for production -telemetry_buffer_max_size: int = 500 # Store more history - -# Consider Redis for persistent buffer -# (current implementation is in-memory only) -``` - -### Health Monitoring - -```bash -# Check buffer status -curl http://localhost:9000/api/health - -# Response includes buffer info (could be added): -{ - "status": "healthy", - "buffer_size": 25, - "buffer_max_size": 100 -} -``` - -## Common Issues - -### 1. Webhook Not Receiving Data - -**Symptoms:** Buffer size stays at 0 - -**Solutions:** -- Verify enrichment service has `NEXT_STAGE_CALLBACK_URL` configured -- Check network connectivity between services -- Examine enrichment service logs for POST errors -- Confirm AI layer is running on port 9000 - -### 2. Old Data in Buffer - -**Symptoms:** AI uses outdated telemetry - -**Solutions:** -- Buffer is FIFO - automatically clears old data -- Restart AI service to clear buffer -- Increase buffer size if race generates data faster than consumption - -### 3. Pull Model Used Instead of Push - -**Symptoms:** Logs show "fetching from enrichment service" instead of "using buffer" - -**Solutions:** -- Confirm webhook is posting data (check buffer size in logs) -- Verify webhook POST is successful (200 response) -- Check if buffer was cleared (restart) - -## Integration Examples - -### Python (Enrichment Service) - -```python -import httpx - -async def push_enriched_telemetry(telemetry_data: dict): - """Push enriched telemetry to AI layer.""" - url = "http://localhost:9000/api/ingest/enriched" - async with httpx.AsyncClient() as client: - response = await client.post(url, json=telemetry_data, timeout=10.0) - response.raise_for_status() - return response.json() -``` - -### Shell Script (Testing) - -```bash -#!/bin/bash -# push_telemetry.sh - -for lap in {1..10}; do - curl -X POST http://localhost:9000/api/ingest/enriched \ - -H "Content-Type: application/json" \ - -d "{\"lap\": $lap, \"tire_degradation_index\": 0.7, ...}" - sleep 2 -done -``` - -## Benefits of Push Model - -✅ **Real-time** - AI layer receives data immediately as enrichment completes -✅ **Efficient** - No polling, reduces load on enrichment service -✅ **Decoupled** - Services don't need to coordinate timing -✅ **Resilient** - Buffer allows AI to process multiple requests from same dataset -✅ **Simple** - Enrichment service just POST and forget - ---- - -**Next Steps:** -1. Configure `NEXT_STAGE_CALLBACK_URL` in enrichment service -2. Test webhook with `test_webhook_push.py` -3. Monitor logs to confirm push model is working -4. Run brainstorm/analyze and verify buffer usage diff --git a/ai_intelligence_layer/WEBHOOK_SUMMARY.md b/ai_intelligence_layer/WEBHOOK_SUMMARY.md deleted file mode 100644 index 0f99247..0000000 --- a/ai_intelligence_layer/WEBHOOK_SUMMARY.md +++ /dev/null @@ -1,200 +0,0 @@ -# ✅ Webhook Push Integration - WORKING! - -## Summary - -Your AI Intelligence Layer now **supports webhook push integration** where the enrichment service POSTs telemetry data directly to the AI layer. - -## What Was Changed - -### 1. Enhanced Telemetry Priority (main.py) -Both `/api/strategy/brainstorm` and `/api/strategy/analyze` now check sources in this order: -1. **Request body** - If telemetry provided in request -2. **Webhook buffer** - If webhook has pushed data ✨ **NEW** -3. **Pull from service** - Fallback to GET http://localhost:8000/enriched -4. **Error** - If all sources fail - -### 2. Test Scripts Created -- `test_webhook_push.py` - Simulates enrichment service POSTing telemetry -- `test_buffer_usage.py` - Verifies brainstorm uses buffered data -- `check_enriched.py` - Checks enrichment service for live data - -### 3. Documentation -- `WEBHOOK_INTEGRATION.md` - Complete integration guide - -## How It Works - -``` -Enrichment Service AI Intelligence Layer -(Port 8000) (Port 9000) - │ │ - │ POST telemetry │ - │──────────────────────────▶│ - │ /api/ingest/enriched │ - │ │ - │ ✓ {status: "received"} │ - │◀──────────────────────────│ - │ │ - ▼ - ┌──────────────┐ - │ Buffer │ - │ (5 records) │ - └──────────────┘ - │ - User calls │ - brainstorm │ - (no telemetry) │ - │ - ▼ - Uses buffer data! -``` - -## Quick Test (Just Completed! ✅) - -### Step 1: Push telemetry via webhook -```bash -python3 test_webhook_push.py --loop 5 --delay 1 -``` - -**Result:** -``` -✓ Posted lap 27 - Buffer size: 1 records -✓ Posted lap 28 - Buffer size: 2 records -✓ Posted lap 29 - Buffer size: 3 records -✓ Posted lap 30 - Buffer size: 4 records -✓ Posted lap 31 - Buffer size: 5 records - -Posted 5/5 records successfully -✓ Telemetry is now in the AI layer's buffer -``` - -### Step 2: Call brainstorm (will use buffer automatically) -```bash -python3 test_buffer_usage.py -``` - -This calls `/api/strategy/brainstorm` **without** providing telemetry in the request. - -**Expected logs in AI service:** -``` -INFO - Using 5 telemetry records from webhook buffer -INFO - Generated 20 strategies -``` - -## Configure Your Enrichment Service - -In your enrichment service (port 8000), set the callback URL: - -```bash -export NEXT_STAGE_CALLBACK_URL=http://localhost:9000/api/ingest/enriched -``` - -Then in your enrichment code: - -```python -import httpx - -async def send_enriched_telemetry(telemetry: dict): - """Push enriched telemetry to AI layer.""" - async with httpx.AsyncClient() as client: - response = await client.post( - "http://localhost:9000/api/ingest/enriched", - json=telemetry, - timeout=10.0 - ) - response.raise_for_status() - return response.json() - -# After HPC enrichment completes for a lap: -await send_enriched_telemetry({ - "lap": 27, - "aero_efficiency": 0.85, - "tire_degradation_index": 0.72, - "ers_charge": 0.78, - "fuel_optimization_score": 0.82, - "driver_consistency": 0.88, - "weather_impact": "low" -}) -``` - -## Telemetry Model (Required Fields) - -Your enrichment service must POST data matching this exact schema: - -```json -{ - "lap": 27, - "aero_efficiency": 0.85, - "tire_degradation_index": 0.72, - "ers_charge": 0.78, - "fuel_optimization_score": 0.82, - "driver_consistency": 0.88, - "weather_impact": "low" -} -``` - -**Field constraints:** -- All numeric fields: 0.0 to 1.0 (float) -- `weather_impact`: Must be "low", "medium", or "high" (string literal) -- `lap`: Integer > 0 - -## Benefits of Webhook Push Model - -✅ **Real-time** - AI receives data immediately as enrichment completes -✅ **Efficient** - No polling overhead -✅ **Decoupled** - Services operate independently -✅ **Resilient** - Buffer allows multiple strategy requests from same dataset -✅ **Automatic** - Brainstorm/analyze use buffer when no telemetry provided - -## Verification Commands - -### 1. Check webhook endpoint is working -```bash -curl -X POST http://localhost:9000/api/ingest/enriched \ - -H "Content-Type: application/json" \ - -d '{ - "lap": 27, - "aero_efficiency": 0.85, - "tire_degradation_index": 0.72, - "ers_charge": 0.78, - "fuel_optimization_score": 0.82, - "driver_consistency": 0.88, - "weather_impact": "low" - }' -``` - -Expected response: -```json -{"status": "received", "lap": 27, "buffer_size": 1} -``` - -### 2. Check logs for buffer usage -When you call brainstorm/analyze, look for: -``` -INFO - Using N telemetry records from webhook buffer -``` - -If buffer is empty: -``` -INFO - No telemetry in buffer, fetching from enrichment service... -``` - -## Next Steps - -1. ✅ **Webhook tested** - Successfully pushed 5 records -2. ⏭️ **Configure enrichment service** - Add NEXT_STAGE_CALLBACK_URL -3. ⏭️ **Test end-to-end** - Run enrichment → webhook → brainstorm -4. ⏭️ **Monitor logs** - Verify buffer usage in production - ---- - -**Files created:** -- `test_webhook_push.py` - Webhook testing tool -- `test_buffer_usage.py` - Buffer verification tool -- `WEBHOOK_INTEGRATION.md` - Complete integration guide -- This summary - -**Code modified:** -- `main.py` - Enhanced brainstorm/analyze to prioritize webhook buffer -- Both endpoints now check: request → buffer → fetch → error - -**Status:** ✅ Webhook push model fully implemented and tested! diff --git a/ai_intelligence_layer/__pycache__/config.cpython-313.pyc b/ai_intelligence_layer/__pycache__/config.cpython-313.pyc index 334773d90d2ff3db57148c61387997dae979ed2b..4d2f9ebce8940f4a20227e83467a9317c7bae12f 100644 GIT binary patch delta 48 zcmZ3^GcPX}0}w1P-N-eOMch$8v^ce>SU)i(v!GPpB|o_|H#M)Mc=G|4OeO$W C`45Z$ delta 62 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 3: + logger.info(f"\n{'='*60}") + logger.info(f"LAP {lap_number} - GENERATING STRATEGY") + logger.info(f"{'='*60}") + + # Send immediate acknowledgment while processing + # Use last known control values instead of resetting to neutral + await websocket.send_json({ + "type": "control_command", + "lap": lap_number, + "brake_bias": last_control_command["brake_bias"], + "differential_slip": last_control_command["differential_slip"], + "message": "Processing strategies (maintaining previous settings)..." + }) + + # Generate strategies (this is the slow part) + try: + response = await strategy_generator.generate( + enriched_telemetry=buffer_data, + race_context=current_race_context + ) + + # Extract top strategy (first one) + top_strategy = response.strategies[0] if response.strategies else None + + # Generate control commands based on strategy + control_command = generate_control_command( + lap_number=lap_number, + strategy=top_strategy, + enriched_telemetry=enriched_obj, + race_context=current_race_context + ) + + # Update global last command + last_control_command = { + "brake_bias": control_command["brake_bias"], + "differential_slip": control_command["differential_slip"] + } + + # Send updated control command with strategies + await websocket.send_json({ + "type": "control_command_update", + "lap": lap_number, + "brake_bias": control_command["brake_bias"], + "differential_slip": control_command["differential_slip"], + "strategy_name": top_strategy.strategy_name if top_strategy else "N/A", + "total_strategies": len(response.strategies), + "reasoning": control_command.get("reasoning", "") + }) + + logger.info(f"{'='*60}\n") + + except Exception as e: + logger.error(f"[WebSocket] Strategy generation failed: {e}") + # Send error but keep neutral controls + await websocket.send_json({ + "type": "error", + "lap": lap_number, + "message": f"Strategy generation failed: {str(e)}" + }) + else: + # Not enough data yet, send neutral command + await websocket.send_json({ + "type": "control_command", + "lap": lap_number, + "brake_bias": 5, # Neutral + "differential_slip": 5, # Neutral + "message": f"Collecting data ({len(buffer_data)}/3 laps)" + }) + + except Exception as e: + logger.error(f"[WebSocket] Error processing telemetry: {e}") + await websocket.send_json({ + "type": "error", + "message": str(e) + }) + else: + logger.warning(f"[WebSocket] Received incomplete data from Pi") + + elif message_type == "ping": + # Respond to ping + await websocket.send_json({"type": "pong"}) + + elif message_type == "disconnect": + # Graceful disconnect + logger.info("[WebSocket] Pi requested disconnect") + break + + except WebSocketDisconnect: + logger.info("[WebSocket] Pi client disconnected") + except Exception as e: + logger.error(f"[WebSocket] Unexpected error: {e}") + finally: + websocket_manager.disconnect(websocket) + # Clear buffer when connection closes to ensure fresh start for next connection + telemetry_buffer.clear() + logger.info("[WebSocket] Telemetry buffer cleared on disconnect") + + +def generate_control_command( + lap_number: int, + strategy: Any, + enriched_telemetry: EnrichedTelemetryWebhook, + race_context: RaceContext +) -> Dict[str, Any]: + """ + Generate control commands for Pi based on strategy and telemetry. + + Returns brake_bias and differential_slip values (0-10) with reasoning. + + Logic: + - Brake bias: Adjust based on tire degradation (higher deg = more rear bias) + - Differential slip: Adjust based on pace trend and tire cliff risk + """ + # Default neutral values + brake_bias = 5 + differential_slip = 5 + reasoning_parts = [] + + # Adjust brake bias based on tire degradation + if enriched_telemetry.tire_degradation_rate > 0.7: + # High degradation: shift bias to rear (protect fronts) + brake_bias = 7 + reasoning_parts.append(f"High tire degradation ({enriched_telemetry.tire_degradation_rate:.2f}) → Brake bias 7 (rear) to protect fronts") + elif enriched_telemetry.tire_degradation_rate > 0.4: + # Moderate degradation: slight rear bias + brake_bias = 6 + reasoning_parts.append(f"Moderate tire degradation ({enriched_telemetry.tire_degradation_rate:.2f}) → Brake bias 6 (slight rear)") + elif enriched_telemetry.tire_degradation_rate < 0.2: + # Fresh tires: can use front bias for better turn-in + brake_bias = 4 + reasoning_parts.append(f"Fresh tires ({enriched_telemetry.tire_degradation_rate:.2f}) → Brake bias 4 (front) for better turn-in") + else: + reasoning_parts.append(f"Normal tire degradation ({enriched_telemetry.tire_degradation_rate:.2f}) → Brake bias 5 (neutral)") + + # Adjust differential slip based on pace and tire cliff risk + if enriched_telemetry.tire_cliff_risk > 0.7: + # High cliff risk: increase slip for gentler tire treatment + differential_slip = 7 + reasoning_parts.append(f"High tire cliff risk ({enriched_telemetry.tire_cliff_risk:.2f}) → Diff slip 7 (gentle tire treatment)") + elif enriched_telemetry.pace_trend == "declining": + # Pace declining: moderate slip increase + differential_slip = 6 + reasoning_parts.append(f"Pace declining → Diff slip 6 (preserve performance)") + elif enriched_telemetry.pace_trend == "improving": + # Pace improving: can be aggressive, lower slip + differential_slip = 4 + reasoning_parts.append(f"Pace improving → Diff slip 4 (aggressive, lower slip)") + else: + reasoning_parts.append(f"Pace stable → Diff slip 5 (neutral)") + + # Check if within pit window + pit_window = enriched_telemetry.optimal_pit_window + if pit_window and pit_window[0] <= lap_number <= pit_window[1]: + # In pit window: conservative settings to preserve tires + old_brake = brake_bias + old_diff = differential_slip + brake_bias = min(brake_bias + 1, 10) + differential_slip = min(differential_slip + 1, 10) + reasoning_parts.append(f"In pit window (laps {pit_window[0]}-{pit_window[1]}) → Conservative: brake {old_brake}→{brake_bias}, diff {old_diff}→{differential_slip}") + + # Format reasoning for terminal output + reasoning_text = "\n".join(f" • {part}" for part in reasoning_parts) + + # Print reasoning to terminal + logger.info(f"CONTROL DECISION REASONING:") + logger.info(reasoning_text) + logger.info(f"FINAL COMMANDS: Brake Bias = {brake_bias}, Differential Slip = {differential_slip}") + + # Also include strategy info if available + if strategy: + logger.info(f"TOP STRATEGY: {strategy.strategy_name}") + logger.info(f" Risk Level: {strategy.risk_level}") + logger.info(f" Description: {strategy.brief_description}") + + return { + "brake_bias": brake_bias, + "differential_slip": differential_slip, + "reasoning": reasoning_text + } + + if __name__ == "__main__": import uvicorn settings = get_settings() @@ -273,3 +549,4 @@ if __name__ == "__main__": reload=True ) + diff --git a/ai_intelligence_layer/models/__pycache__/input_models.cpython-313.pyc b/ai_intelligence_layer/models/__pycache__/input_models.cpython-313.pyc index 7e67f69de05803214a8b5aa4038eb03bff68d4c1..fb3dd0527d8365471f8401a85bdb7015a21cce2e 100644 GIT binary patch delta 2169 zcma)7UuaWT7{4dEP4llw+BE$W{~g;@o7$$xj3>X?iuujpyFB zHVj%}W5Qrm&r5~Df^6X41bx^Tj14vzV-E>sLNAOx>|x9ox7jiFB0Jwn*Sf8Of&9*Q z&-d?l&Uemt?)H4T-}khp#x3#Rhj&(|}l=XKOs=RjT@I)HCi?YO1*HxKr`%hOvsru`AVzQ}HYA zKAF~xySXm#oUT@Q`o{g^)$DE{;#5MkRwR08!{`G0-gexJ+cRpuD??^TCdxwg-ubQ( zjS`$9X{x4FQ`2*pswTnhdt@(Q;{P-eH*PM21+d6iH^Z9L%}% z+&dw>3!TGMpHL^XjAqVZeG)@HoF{Zrr&%?}lW?cW$qL=58)k5bblGL+V z&V!_IUNf;_>Up4nXSG~PpT)`7d46hkVs7$=Rj21oEvse{+%!?KD8$ZlYd#B`lrdF2 zW8ewZAe<}Tr5s7~O`9S3C^jDaQ@Q$=1kFEIY7nNeB?2j)D+zc+u z@s-y0d%^p`PlBa$bLG}TNp4>1U6uSBqS!!O`_3JfrS;CmV*L4OdW4ZdgK z^M!l|Knu{u{Jv`_!fyE9L^1ZvH@cSAx4umtvFRdNoXz{UZ{Msi=O7+HjD6;hp~!>B z{x*bqS)ld_I&`~kIAGcNZJ>v_g(X7|VTroowN&-%sluWnBM=J%h@_(+^{~fvE$s;q zF9U4G;SwkSdfEQqSA0!>1>Zv_*v@`YWZLV9$ch?7PsrXpp_y(AJOw8xH^^iP1?A(~LD7s8IRu#drxSS0RL0;%1~Y z&;ksyzQ&OXDNT_2z!zS7Od99m8F7h6!U;uHT-1|xM?KcoQ#q<7r$}mqWJs2nbZ$&D zrv~(#Nv@l}3~|%Ku`x0+rR!I5UY*P6Y6>U0Ru%1;cgl)4Hve(QD(~|YT0WTZAhB3s zn{t%*@(dBp_Wv#$+qJe^u%b#E@va|``HjN8Y`L-!2LP5~?BM|Y^6X0d5VS_E9EZ+U zYT65J4Yw7OrGppBcxd7L;+{wHP^I+E zcki;CuVnSqbcXcPYupV18^+;RJ3{C;n;pr2IwiF0N2#MMb*wt7(G*&mTa{p3t#_a? fbd!I<$j&wmHLprCx{OwRc61RH`~Hw%6lDJf=4ltG delta 1791 zcma)+T}&fY6vsR5w6ukO0R04kfl`+43I$q0bhGO&%ZEU4vxTg?K}(srv=iG-y)y;d z7{NrN4?eJaUwB%H(FCK(lI#n<_@q9VVA>E(I$7V1FD5QN=#%%{VqB3mHtDZt=6~+_ zpL6FK*bVg4e(N}6bKh|WxQBPn{m7?15ijZFzU3a0&ZchiyseG&8vUCM z32UM$VISvlOP9?(hJ+~rnv-cwOmm&m+)Q&Yt@)JJ!Zat-S|8Iq3G=uS zyO!MvubDczsihC(gdaDj1Cb#4+$eBY$f5BC5;Q%t8NGtAB9-!~oGc1xZA}(slov}v z|Ipyz@Y6y@PG=AnWK~#2nuc)XQ^K*Sx2}es3lcHIYG>%a2g3&w2rd6 z9bq+Dzf<;36;bx_wpCHVNcXSN+exLM$vL@PH()~9H=imDXv#n~RS-laugWje`gB~q zMZz+5OtPExdu9Jkl+s{&Ivo-!SMRp#>5UZujf?Mxo=N-fZ_BkqcOV&$?_4WO;rZSxV2YT@%x;2>6+ z7pq~Z!ng0x8d6ONL+ZX*neIzgg_R2L+o9zHQZH2I@_{vPVM701+acFMzO&urE|5NZ+>8S>i>%sbxIyxn{nkk@&CpAi00metV-Sr1 zSTD~))J-lo20~ACk3$ZigOnTJsk_I?yd_9Fn(lLbR7 zCqs?^?dC-)tX^(_02n1*tzXr9xk2uEgT&}5a94@y zdD{XEGpOAsRRmT$D)#_5CpKiZI?1e^1$hSHz{W=OAp(a8=vT+(+63#2O;pJI=cok%zX z_0d?l6}V*U|M~Lh){8ksDrV6ooTVuo{i^Shd%-rRm6N%h(y;-4kA38u;O)lW3_Q2O Hu{ZBe?Gvsk diff --git a/ai_intelligence_layer/models/__pycache__/output_models.cpython-313.pyc b/ai_intelligence_layer/models/__pycache__/output_models.cpython-313.pyc index 637d70caad32ba0fabc523a7ebf8157255c353d9..78b53da3cb6f769a2a3c4549a16a31b0f7eda911 100644 GIT binary patch delta 48 zcmaE3anFM5GcPX}0}$Lw-NYSx~C)lAm0fo0?Zryg8AXTM__y CNe_?! delta 62 zcmca-@y3GdGcPX}0}x0@ZsamzR*lsUElw>e)-TE|&L~aFO-#v1EYo+%PcF?(%_}L^ Qch4;GC{5ZN%*-tb0A>0VFaQ7m diff --git a/ai_intelligence_layer/models/input_models.py b/ai_intelligence_layer/models/input_models.py index 50cfe41..ef0ba66 100644 --- a/ai_intelligence_layer/models/input_models.py +++ b/ai_intelligence_layer/models/input_models.py @@ -7,14 +7,13 @@ from typing import List, Literal, Optional class EnrichedTelemetryWebhook(BaseModel): - """Single lap of enriched telemetry data from HPC enrichment module.""" + """Single lap of enriched telemetry data from HPC enrichment module (lap-level).""" lap: int = Field(..., description="Lap number") - aero_efficiency: float = Field(..., ge=0.0, le=1.0, description="Aerodynamic efficiency (0..1, higher is better)") - tire_degradation_index: float = Field(..., ge=0.0, le=1.0, description="Tire wear (0..1, higher is worse)") - ers_charge: float = Field(..., ge=0.0, le=1.0, description="Energy recovery system charge level") - fuel_optimization_score: float = Field(..., ge=0.0, le=1.0, description="Fuel efficiency score") - driver_consistency: float = Field(..., ge=0.0, le=1.0, description="Lap-to-lap consistency") - weather_impact: Literal["low", "medium", "high"] = Field(..., description="Weather effect severity") + tire_degradation_rate: float = Field(..., ge=0.0, le=1.0, description="Tire degradation rate (0..1, higher is worse)") + pace_trend: Literal["improving", "stable", "declining"] = Field(..., description="Pace trend over recent laps") + tire_cliff_risk: float = Field(..., ge=0.0, le=1.0, description="Probability of tire performance cliff (0..1)") + optimal_pit_window: List[int] = Field(..., description="Recommended pit stop lap window [start, end]") + performance_delta: float = Field(..., description="Lap time delta vs baseline (negative = slower)") class RaceInfo(BaseModel): diff --git a/ai_intelligence_layer/prompts/__pycache__/brainstorm_prompt.cpython-313.pyc b/ai_intelligence_layer/prompts/__pycache__/brainstorm_prompt.cpython-313.pyc index dc166c6785cc1a0117743f831ed1635c65eebfbd..be44da24e2021f0b370a73570a6ecec11af92823 100644 GIT binary patch delta 4827 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 HsP6SYRSA@D diff --git a/ai_intelligence_layer/prompts/brainstorm_prompt.py b/ai_intelligence_layer/prompts/brainstorm_prompt.py index b45890d..aeb66f3 100644 --- a/ai_intelligence_layer/prompts/brainstorm_prompt.py +++ b/ai_intelligence_layer/prompts/brainstorm_prompt.py @@ -11,12 +11,11 @@ def build_brainstorm_prompt_fast( enriched_telemetry: List[EnrichedTelemetryWebhook], race_context: RaceContext ) -> str: - """Build a faster, more concise prompt for quicker responses.""" + """Build a faster, more concise prompt for quicker responses (lap-level data).""" settings = get_settings() count = settings.strategy_count latest = max(enriched_telemetry, key=lambda x: x.lap) - tire_rate = TelemetryAnalyzer.calculate_tire_degradation_rate(enriched_telemetry) - tire_cliff = TelemetryAnalyzer.project_tire_cliff(enriched_telemetry, race_context.race_info.current_lap) + pit_window = latest.optimal_pit_window if count == 1: # Ultra-fast mode: just generate 1 strategy @@ -24,7 +23,7 @@ def build_brainstorm_prompt_fast( CURRENT: Lap {race_context.race_info.current_lap}/{race_context.race_info.total_laps}, P{race_context.driver_state.current_position}, {race_context.driver_state.current_tire_compound} tires ({race_context.driver_state.tire_age_laps} laps old) -TELEMETRY: Aero {latest.aero_efficiency:.2f}, Tire deg {latest.tire_degradation_index:.2f} (cliff lap {tire_cliff}), ERS {latest.ers_charge:.2f} +TELEMETRY: Tire deg rate {latest.tire_degradation_rate:.2f}, Cliff risk {latest.tire_cliff_risk:.2f}, Pace {latest.pace_trend}, Pit window laps {pit_window[0]}-{pit_window[1]} Generate 1 optimal strategy. Min 2 tire compounds required. @@ -36,17 +35,17 @@ JSON: {{"strategies": [{{"strategy_id": 1, "strategy_name": "name", "stop_count" CURRENT: Lap {race_context.race_info.current_lap}/{race_context.race_info.total_laps}, P{race_context.driver_state.current_position}, {race_context.driver_state.current_tire_compound} tires ({race_context.driver_state.tire_age_laps} laps old) -TELEMETRY: Aero {latest.aero_efficiency:.2f}, Tire deg {latest.tire_degradation_index:.2f} (cliff lap {tire_cliff}), ERS {latest.ers_charge:.2f}, Fuel {latest.fuel_optimization_score:.2f} +TELEMETRY: Tire deg {latest.tire_degradation_rate:.2f}, Cliff risk {latest.tire_cliff_risk:.2f}, Pace {latest.pace_trend}, Delta {latest.performance_delta:+.2f}s, Pit window {pit_window[0]}-{pit_window[1]} Generate {count} strategies: conservative (1-stop), standard (1-2 stop), aggressive (undercut). Min 2 tire compounds each. -JSON: {{"strategies": [{{"strategy_id": 1, "strategy_name": "Conservative Stay Out", "stop_count": 1, "pit_laps": [35], "tire_sequence": ["medium", "hard"], "brief_description": "extend current stint then hard tires to end", "risk_level": "low", "key_assumption": "tire cliff at lap {tire_cliff}"}}]}}""" +JSON: {{"strategies": [{{"strategy_id": 1, "strategy_name": "Conservative Stay Out", "stop_count": 1, "pit_laps": [35], "tire_sequence": ["medium", "hard"], "brief_description": "extend current stint then hard tires to end", "risk_level": "low", "key_assumption": "tire cliff risk stays below 0.7"}}]}}""" return f"""Generate {count} F1 race strategies for {race_context.driver_state.driver_name} at {race_context.race_info.track_name}. CURRENT: Lap {race_context.race_info.current_lap}/{race_context.race_info.total_laps}, P{race_context.driver_state.current_position}, {race_context.driver_state.current_tire_compound} tires ({race_context.driver_state.tire_age_laps} laps old) -TELEMETRY: Aero {latest.aero_efficiency:.2f}, Tire deg {latest.tire_degradation_index:.2f} (rate {tire_rate:.3f}/lap, cliff lap {tire_cliff}), ERS {latest.ers_charge:.2f}, Fuel {latest.fuel_optimization_score:.2f}, Consistency {latest.driver_consistency:.2f} +TELEMETRY: Tire deg rate {latest.tire_degradation_rate:.2f}, Cliff risk {latest.tire_cliff_risk:.2f}, Pace {latest.pace_trend}, Performance delta {latest.performance_delta:+.2f}s, Pit window laps {pit_window[0]}-{pit_window[1]} Generate {count} diverse strategies. Min 2 compounds. @@ -67,27 +66,19 @@ def build_brainstorm_prompt( Returns: Formatted prompt string """ - # Generate telemetry summary - telemetry_summary = TelemetryAnalyzer.generate_telemetry_summary(enriched_telemetry) + # Get latest telemetry + latest = max(enriched_telemetry, key=lambda x: x.lap) - # Calculate key metrics - tire_rate = TelemetryAnalyzer.calculate_tire_degradation_rate(enriched_telemetry) - tire_cliff_lap = TelemetryAnalyzer.project_tire_cliff( - enriched_telemetry, - race_context.race_info.current_lap - ) - - # Format telemetry data + # Format telemetry data (lap-level) telemetry_data = [] for t in sorted(enriched_telemetry, key=lambda x: x.lap, reverse=True)[:10]: telemetry_data.append({ "lap": t.lap, - "aero_efficiency": round(t.aero_efficiency, 3), - "tire_degradation_index": round(t.tire_degradation_index, 3), - "ers_charge": round(t.ers_charge, 3), - "fuel_optimization_score": round(t.fuel_optimization_score, 3), - "driver_consistency": round(t.driver_consistency, 3), - "weather_impact": t.weather_impact + "tire_degradation_rate": round(t.tire_degradation_rate, 3), + "pace_trend": t.pace_trend, + "tire_cliff_risk": round(t.tire_cliff_risk, 3), + "optimal_pit_window": t.optimal_pit_window, + "performance_delta": round(t.performance_delta, 2) }) # Format competitors @@ -101,15 +92,14 @@ def build_brainstorm_prompt( "gap_seconds": round(c.gap_seconds, 1) }) - prompt = f"""You are an expert F1 strategist. Generate 20 diverse race strategies. + prompt = f"""You are an expert F1 strategist. Generate 20 diverse race strategies based on lap-level telemetry. -TELEMETRY METRICS: -- aero_efficiency: <0.6 problem, >0.8 optimal -- tire_degradation_index: >0.7 degrading, >0.85 cliff -- ers_charge: >0.7 attack, <0.3 depleted -- fuel_optimization_score: <0.7 save fuel -- driver_consistency: <0.75 risky -- weather_impact: severity level +LAP-LEVEL TELEMETRY METRICS: +- tire_degradation_rate: 0-1 (higher = worse tire wear) +- tire_cliff_risk: 0-1 (probability of hitting tire cliff) +- pace_trend: "improving", "stable", or "declining" +- optimal_pit_window: [start_lap, end_lap] recommended pit range +- performance_delta: seconds vs baseline (negative = slower) RACE STATE: Track: {race_context.race_info.track_name} @@ -129,12 +119,11 @@ COMPETITORS: ENRICHED TELEMETRY (Last {len(telemetry_data)} laps, newest first): {telemetry_data} -TELEMETRY ANALYSIS: -{telemetry_summary} - KEY INSIGHTS: -- Tire degradation rate: {tire_rate:.3f} per lap -- Projected tire cliff: Lap {tire_cliff_lap} +- Latest tire degradation rate: {latest.tire_degradation_rate:.3f} +- Latest tire cliff risk: {latest.tire_cliff_risk:.3f} +- Latest pace trend: {latest.pace_trend} +- Optimal pit window: Laps {latest.optimal_pit_window[0]}-{latest.optimal_pit_window[1]} - Laps remaining: {race_context.race_info.total_laps - race_context.race_info.current_lap} TASK: Generate exactly 20 diverse strategies. @@ -144,7 +133,7 @@ DIVERSITY: Conservative (1-stop), Standard (balanced), Aggressive (undercut), Re RULES: - Pit laps: {race_context.race_info.current_lap + 1} to {race_context.race_info.total_laps - 1} - Min 2 tire compounds (F1 rule) -- Time pits before tire cliff (projected lap {tire_cliff_lap}) +- Consider optimal pit window and tire cliff risk For each strategy provide: - strategy_id: 1-20 diff --git a/ai_intelligence_layer/services/__pycache__/gemini_client.cpython-313.pyc b/ai_intelligence_layer/services/__pycache__/gemini_client.cpython-313.pyc index cd988b64f952a9a660147ad7c66053989f89b9c3..13195d89a2a4a25041e6ac2f5ed47cf83511e86b 100644 GIT binary patch delta 48 zcmX?LvdM(&GcPX}0}$Lw-N;qPA|9w8TAW%`te=>YSx~C)lAm0fo0?Zrym>9lWKjTf CdJs?m delta 62 zcmdmFa=?V^GcPX}0}x0@Zse+CQH|FRElw>e)-TE|&L~aFO-#v1EYo+%PcF?(%_}L^ Qch4;GC{5ZtpJlQr0AD>6@Bjb+ diff --git a/ai_intelligence_layer/services/__pycache__/strategy_generator.cpython-313.pyc b/ai_intelligence_layer/services/__pycache__/strategy_generator.cpython-313.pyc index 07703029d0089c22bfad61c09a9f5cd0dbf686de..36adeca42098645fb6ca91cce537d172fe121ac5 100644 GIT binary patch delta 1108 zcmZ8ePfQb882?_UGo8+KrnNu;TjjM%)+(ezQ5F>1VyPloG@Xnw)<0=v3N>XQGsQ$U z5hE8osd+Jb-DEc=#=~Y4514o`NaD$40F8<3!GjlXbs@VZdhiW6V)&AI@Av(_-}}w? zzV{A449jbhBmfQh{`$B}{97Jo8qXwDQ!~s&akM-&oybmJo}SS3s!6UhFM=GE&e3me z-UYA+Fx(@{9BxJh);6(2MTgKPl0k8ir|#6OA3$Lg_b_{s(-{mZ6N9tCL)5?y-F-?d zP{n5a$g7YKIQ-V0uiL^7Qr!wm)pm2+B3}tTjK@r*M7SJ6FYg!O^WD!j@b zp{*B1fI_2lM)5k=(U3|t_C0kj0lG)!>fi*X8NV9}&ENI%K~VWDI*fF$>dA*GPyXTt zX2l;SO?d#pnAn^x;0h4Org` zUjJ?JmS}}KA0@5sq1EI`t8>^IzF^f~e92!Tx-Tr<%Dr!Bv)WUu4I}e8^2Dd^sBsW# zniu{Fwa({iF1M5+{nGx~J6Ha!`~BP>Ef(&uItHxzV=wvaItP*V*WvU^IQ?|zYWSSR zH&H4Ov)cQX{rwi-|FPTMEPSGU)bCmYBxTUsK zoR-H?CwVM~QH;Ej+mhXE>Rkj|(Vpc7fK6rRoQ+Us5agy1+1PVgj6fIvbp&JT8!n5Ym3Q5@v$su0t1MB8RHgfZZac95`@6>LD#rTZtYz8??52>PkEB zXWsX{H*enD4m^eK7cLhM$hZIEP60W;ai66JbEA^3X!@w8>V|g{Sotqo8D$YBQLlKU$0v;4Ul8OB{LPvyZh9Rzu5lBM(1Pyp3!f5!SnL)Hwnv;=iSlMQ9*GR$bQ zTGEXbtw`KQj~=bqB;5?CbU;H{R*H(DU51*HUD0wndTAdFReY}k=4Qn=;kkuVr{Su$ zQd~7Ep7XmBE-R%aqY#A^8_X7DO-GQsH0VewSw&q_a)aGlcxl99R&UZb z7HV(|2MO^T*n6(dCAMwA=U?Z4^$o7iZMQM54EljTT*bb_UxjZ~ZpZN9sXJ5H8?STo zJ2ucW@;GqvQQ+j=eYLz<{Falf}` zF4T5EGCN6(2O&NlVjoN$i4U`z2W-T%Im{BzW{8BM&xAtnq9lUhhMG;whM}pM zvZ3hdv^I#Kb2vQ1#-8>-+!uM+6WwNsMcUTYzil@)4)8sHIf$m2&s1}fWyq&ZqpI@? lbqLX}Bkd8>zdIi0CZU{h`KdU1%XQilr>NK;z+89H{{qXaa^L^} diff --git a/ai_intelligence_layer/services/__pycache__/telemetry_client.cpython-313.pyc b/ai_intelligence_layer/services/__pycache__/telemetry_client.cpython-313.pyc index eca2428dbf4d6b16d5177a40e1caa307a3851611..c26328090bf1ba2883e34e32b408ed4784fdf534 100644 GIT binary patch delta 48 zcmaE?c2$k*GcPX}0}$Lw-N+TsEFPjCTAW%`te=>YSx~C)lAm0fo0?Zryt$A0Dn9^s Czz}%= delta 62 zcmcbr_E?SUGcPX}0}#kYZsdw*R!!0mElw>e)-TE|&L~aFO-#v1EYo+%PcF?(%_}L^ Qch4;GC{5bjz/dev/null + sleep 1 +fi + +# Start the AI layer +python3 main.py > /tmp/ai_layer.log 2>&1 & +AI_PID=$! + +echo "AI Layer started with PID: $AI_PID" +echo "" + +# Wait for startup +echo "Waiting for server to start..." +sleep 3 + +# Check if it's running +if lsof -Pi :9000 -sTCP:LISTEN -t >/dev/null ; then + echo "✓ AI Intelligence Layer is running on port 9000" + echo "" + echo "Health check:" + curl -s http://localhost:9000/api/health | python3 -m json.tool 2>/dev/null || echo " (waiting for full startup...)" + echo "" + echo "WebSocket endpoint: ws://localhost:9000/ws/pi" + echo "" + echo "To stop: kill $AI_PID" + echo "To view logs: tail -f /tmp/ai_layer.log" +else + echo "✗ Failed to start AI Intelligence Layer" + echo "Check logs: tail /tmp/ai_layer.log" + exit 1 +fi diff --git a/ai_intelligence_layer/utils/__pycache__/telemetry_buffer.cpython-313.pyc b/ai_intelligence_layer/utils/__pycache__/telemetry_buffer.cpython-313.pyc index 242d3146df390fbd4ad0c1a5dd44c23ec50180dd..c6f317eee2c362503de6e3a1fc81a68b5abcc56e 100644 GIT binary patch delta 48 zcmew>`CgLiGcPX}0}$Lw-N@y}EFP#ITAW%`te=>YSx~C)lAm0fo0?Zryt$OQgA)LF CK@bZ7 delta 62 zcmaDa`B#$bGcPX}0}#kYZsc-fR*lyWElw>e)-TE|&L~aFO-#v1EYo+%PcF?(%_}L^ Qch4;GC{5a&&fLKX0A&CatpET3 diff --git a/ai_intelligence_layer/utils/__pycache__/validators.cpython-313.pyc b/ai_intelligence_layer/utils/__pycache__/validators.cpython-313.pyc index 3a18a8c1baae4add5f759d816878de63f95edb2e..35f91af4fb1b977c1aab448624387fa04a094254 100644 GIT binary patch delta 970 zcmZ`&O-vI(6rSzR78Xir>stPlp*Eu9@dAfy@~8U#g42}T00bSMkUwwYZ-dcnlR za50h01wHA(#Ggb_qh5>}Z{FH)p!8tyVuBZoHJ&^;vnAo=JACuKH{ZAK&1`0Otk7Il zbUFn<)re;ta zs&cAXMc0u^ZnVM$1kgnPaIvHSu(Mc7YAdcx(cb7)Dj zF(KzC_0}$h)_Gan~fS5JPcfAKdE>#_7mGo5m5}GY$3sMaWbJrFvqaxrpO4Hp$vh1U?oNH(wWzqb=1olR|gziue=GD$VX z>rc@(_%wrF3OYvt;zpMDlb=o9%V|c>FkttC&r(QCoAEt@-7LF*kHuDGhfR#DDeSl? zNLD;Rm%NMphe>nHNv!B|c~3IMx~r2Z#h6@Fw0Lq+vg00RKEPm*xr9B#0p<|)l{#xe zUitg@1SL8qfBk_8v3^qwWyR2nuw5apc{eH=a^Axa+^dmHr9bENt+j7B5ARnBJg9@4 zwQbqjwv~aeftI^nYw~JOrm}DS+$VpwqmQ!ES;Mr$dC5t>N#TkiHk4J-41AeHjvZY_ zG`|hDY$MxtcfsPaA7AgtbPi|RFBSkzGNV_DY%N*=?=iOvev>uW`?;Lgm#GP7y(bD* zmTrqR1v?{jF2PxJFlzdW7Dh{>bklZH6=rWM&l^iA@+_otGHE>C>uTfrxnF>xDg6id C0T@UC literal 11377 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 K{)54sF7ba8+4U9x diff --git a/ai_intelligence_layer/utils/validators.py b/ai_intelligence_layer/utils/validators.py index 1dcdd68..3f938b7 100644 --- a/ai_intelligence_layer/utils/validators.py +++ b/ai_intelligence_layer/utils/validators.py @@ -80,133 +80,25 @@ class StrategyValidator: class TelemetryAnalyzer: - """Analyzes enriched telemetry data to extract trends and insights.""" + """Analyzes enriched lap-level telemetry data to extract trends and insights.""" @staticmethod def calculate_tire_degradation_rate(telemetry: List[EnrichedTelemetryWebhook]) -> float: """ - Calculate tire degradation rate per lap. + Calculate tire degradation rate per lap (using lap-level data). 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) + Latest tire degradation rate (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 + # Use latest tire degradation rate from enrichment 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" + return latest.tire_degradation_rate @staticmethod def project_tire_cliff( @@ -214,65 +106,27 @@ class TelemetryAnalyzer: current_lap: int ) -> int: """ - Project when tire degradation will hit 0.85 (performance cliff). + Project when tire cliff will be reached (using lap-level data). Args: telemetry: List of enriched telemetry records current_lap: Current lap number Returns: - Projected lap number when cliff will be reached + Estimated lap number when cliff will be reached """ if not telemetry: return current_lap + 20 # Default assumption - # Get current degradation and rate + # Use tire cliff risk from enrichment latest = max(telemetry, key=lambda x: x.lap) - current_deg = latest.tire_degradation_index + cliff_risk = latest.tire_cliff_risk - 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 + 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 diff --git a/hpcsim/__pycache__/__init__.cpython-313.pyc b/hpcsim/__pycache__/__init__.cpython-313.pyc index 63927072978b6891f73f2938486169592991b1e1..e4f1abcbf0929597c6ce898f6825777008158216 100644 GIT binary patch delta 45 zcmX@axP_7HGcPX}0}z~Woyg@QZlWJroLW?@pO}(aP^#~epIn-onpaXhvDzH~Daa0x delta 59 zcmdnOc!-hfGcPX}0}ve3oyg^*>ZBi9oLW?@UzAy#QJR#Sn39oLrtgxUT$-DjS5mC+ No>}5inlv%j9RMDV6X5^= diff --git a/hpcsim/__pycache__/adapter.cpython-313.pyc b/hpcsim/__pycache__/adapter.cpython-313.pyc index 1094a6d18f91851c6e14f04a88093f12c1f98270..e1e77f80da4c7235b149292e7e629e09dbde91bf 100644 GIT binary patch literal 3600 zcmb7HU2Gd!6~5yc+v9N@$BrG39lM>ROm)gFrw)Fy8vb)Ae5k5c(UP*u&LPn1dHUd4OajGcTb91~U@X zT`##7Sj;YPn1i`nW)tW(huv#j+Vg}urWxW_b-ik;W~r(hX&2$HmGUNGXY@v%p@{DA z!!L^vbMRY`x6rM=2WSGedYO@3GCSb{oqL%9>9(ZDl3q*lmh@Rt0K7lztMr{iIwK#G zk1Kr&y6BPvkQRTNyUpk>#WjlLemOYVElg*Rp;adng`Mo`I%^?a@_>WnjvzS)$yvyd z+~*+Oiu+8rFts0{Rj*y9;jxCyOV1O^5K`^3OqX|y9+x1EteeAd20u;=M;TM&c^5D;g;i}^q(TS0>4G(OYm@pfFIDgWNKxtqM5iM)zn70surXou2!U*CCM+b?KHy@)^ho3rBoQ{%~#+p2rUlL_=^zJo*OO+okV`nR};imhyL`VhI~2Tz)gZUeeb( zPkX09bh||sJ{{OK%}l!qdq-;!Z$T@n^|DErJH*><4#e998+2z$lA29&VL~x9Y)q(y zQmsC5t(vdX3ua8*xH&7ADssuxCe~|tqf`Ne3Y`XLYYoEHASW{7Tvd25y3H$lxm?Yw zWn(%^AtzxQ;}VEZo9LsE`19e{hqvVIu|J2-Y|ONS1Dn@>_QP%OL*~~)OZsX%c5LhW zuiyJ%U}WEo!s$H^8i;N5AAmFJFh+@Za)C}A+SaI*jzj*4r=k@~^$PJU&Ru&^Sv=&k z3%+y}OW-k|=6f!}m)?Oivy^t>F>oR*^y0LSuqEAuyVs~`a~M}K5x%XK>m41Rr#N>J zI=D%Ml2OtPQ`Petao20mI|%nmwOS^gdJS5$mJZ;w6}DP8@mbJ%QgVtstnIKCs{~BH zLkx=h9&l-otC)UylEME$4yG57e$5!uaG3vYqEALq>?+fao!DO889GlQ7f5`TL?=mn zi6mx8YLP@P?hgt10bmv2ZUs*@Z%}i5cQCfazcIbR?+M7w z|779E3+*9^{>4wVqsORBoN1@B?MQMzUJ91Joz^1)oUlzK>eKN9^?(ZpRb% z`>AF48K04MQu=_8Q`5*Zrg(c$yw6ih{4?W6aF2fPzm`F72mEs!dI$OEQs|wie{K@J zJM5nipm)#t=f}`{ZvXrh^j;=B$FPqOLnRlUOR$gP43(+y+*jB~=NT#|!}9|B*vC*A z2+xnQk4G3P$HMbd?Bh!el~=;o!|b0z49KN4XFWZi_0xZmR^iEn4+*{TUE;yks}x^` z4AUMW$&(1!sAr8o*w>krCO+$wP`#9YqZ51 Y^a~nFXPCdB(SM-iKRrc;30f}y1(GCdW&i*H literal 3427 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 delta 1247 zcmYk5OKclO7=UMXcGv6m+Bgr#n>0@1JV*>}oaWWKq)j46YC=RrS*21*RCi-<>=k~5 z@g}8_phY0Ha7r~)0&(eqL*SYV;zZ*JE(ir#BJQe)Ba~B6Qo)gb94TT~^Y1tRd*|0| z_uY#Ij{JTV!Slnp9{YM7A>W$)m1xfTgCJpx5crkmC9IIYGzEwE)`RDF=M^wsunG$ z(m9i6EjPTnZsk*@HODU4nIq5Np>E)|WjeV4nC!xu znRdQL3o)6^i{tE5aUk9e|9AuoxtHUMWhfMhMd3{&0#~#5rj7IBeEcAs301o0f*Lx-9uE#PONf$eZLbpC<*tf~E=spprbp2x3Oo8k|Y zV}`@zY72-3$7!ehr8()7=jsxP`PM*lsIvL&_O(+sw! z!AbUsI&ilgf83Syqpzl$XWH?Tgg&F8-3FXIEAKWE&LKTHOm@%VKquHw8We`j^v(;d z6HBvz^Q<-Sjj!(8$bGxn&w;S?2H-XJSD;UXxJ&F-u%%;yTU$WH@bh8*5$IK}(mD6A z&w}sb3rubp$PICAJr}>I^$uJ)Yr(bIpyOP_lh6bge4dmy}IL_jYZ>oO_7?0Mo{|9}bKI8xZ diff --git a/hpcsim/__pycache__/enrichment.cpython-313.pyc b/hpcsim/__pycache__/enrichment.cpython-313.pyc index e4ecd4cdd233cf9482d820473158d8a167ae4c1e..3326379dab35f44009017f88a930136a301b7153 100644 GIT binary patch literal 9497 zcma(%TWlLwc0+PV4TmpD6e;RG5+z5JWy+7pw)}`J%T6N2k3{!_r-l9#IX3;q9rjZM@mfHexfOet&6xyOi^RYnlkrpJzt(d9WdeLA5q5Y6!w<)$C z={a{eBrVC!4y4O_&pr2b?s?BVbh&s2ob1n>qy-sN4QBlu~Rtv0#6*GRkVq9A}88KK5QjU z(Lr37ECHs65uL4!=o&WbYYpo^3~|#wH}rXoKH^ERq54h42$?j8lul=IQZAWEDEVx{@*#7HAtsR_X3<0}eLy?HUb6Numu#YC#6;>u_7W#r zp90JpsGUM>2o>uHFLHomr!AzX16oenasf55&rMq%;-am3+G-$firGk8UZ7x8$fvo^ zrb#k(StcUTPu}c-6_%bpFTru(UlE8DlLZC41Sv)`iXx<>tRl>3h>#<4Ixa{+$LmT` z3Bxsb;_|$-n94=xrC2UQt}i)|zBBWwjFg+z>@YH#OD@QY#$#2<%5q#Gc-fi-*3;@^ ziv;G(0TPj;QbHy!+Nb1*=6kF!mRZPV7Sr)f6CyNGd?g2SL}9BW7lr9J4`COd1GY$E z8(86dCcPv92+s6+G%YR2pD-6f^%@tYjc8QkqtS&-d@&_M-5HI(u_&bw4O*UPbUsOx zTq>EC(-{ENMWgXdEE*+NT!u}-If);ufLX(gfY1z3f-FJWu;{>|3yW?jG+z|AoJ+={ zQZ7f5bBj4yfkAi;58AF z!jMca5_`kh>%; z>`1A-qrf)ZjHzr#sXJ6)Tb2*0Y^Wr37udjZP-VMI{s5p_R5noZcVO+G*bcJSSn4sR z$9sjx^e_$O<4lh%FIrj|VinyY+sa&FiB0r~R$8kQ>qQ%_abkm5M{9PmQRHZi7rmmL z)*ND!$kUor^ob5ybBRvkPPjsTjWgJKGYrcw{W%Zx+6PoxOaUv$g(|HmD+7P*OUYN| zl<@2UAt$Hg1vy8qhk1T17L&6%%JgLfR4u|h$t(y{l9HX13Arx3m;^D13kf+b6PR8R zvN8b)S&-5oJRn8M#O0jA=PpY*!PtTT`w@_h7j?BIrK*y|$B?a!2kB@*I_`(chs;c+ z&rpvMCRKrn47AFWXP5-&#tR_}WdqGhrC4i>&Pj@le2h{-h*fp~NOt(A`~L}uO6~q5 zrYV4Le?*gLHkABu`CMsp_0&%oWf9pOqbyIrwN~b3XHb0U^8{QlR6FLie&;A|mr=Hv zy6;#E%;EO6rjUBu9^>BAb>B%9$1b$wzH|4^8WU{XfPKbcY~u-(a#xLWg}!`*nL-`D z?TN4z3O;xJ|sfb(~qscG8~Xin`mQ-v!8X8Oi%Go>mNn9lCn z?=)7L-oI;qg%UGp1nBdGJ@0aQ?ZUeZ)M2mXJOXr|322 zkGN{!?i#qK23{{V8Tg_vY)d!5db`O5D(7ua#1b(_`2I3f0^KY6FPg;Wh+PbXtzwJV z8mWtLFyGL&fzX@|R#t5T-1e|J-56RQzb1xI>T#EDP3)b38FpZZp@oi=ktzwv; zl5>j$oJQTcf|;$DQem&|NyakZCcl}ZqsJz|m%J{MFt7WA>BR*!r)WEL2MM`H;|wdM zxumOLUK6tJIDTauMzb$SZ|X43MLj{oJJf9F&z_lxoY&kGoJ!79o2O_Ex(k?-7r;KnQO+?Z1 zYkU^`2lFI8^V+P`m1#m~RU|6pu)A7tTt)a$Mh{okqPSPyjmmyF& zL#~Ic)JLSm&>V&XjH@?XlcgN^>`|a+Jc-L|jny+nV{%GKE-IRBECm5kW)qEE$f`Nw zBnc#>-fq>|&Sn%k19eZ!6htb+I*R6>UzAf(A};_@lIcVgb{hi+6Jsol48TX2&uGr_ zywt&fu14ejqm}#DoD{u${h&+sk$lT z0HEkjwoS@rA^6ztTccYI*8|HR#9Aki2OTW95DX}GT$Z=eUvq1L3UK@NwN-j{xyRN@ z=gteY5{y-Jf8QOIAy8QWUWp=ULtb(Sw@SGjg@_!%L(+pd4bF!Mfshe|AIIVZ7SCWY zip41?wE8Npj69)~FA~m78IwGVsLvrPU*$eBfza<`fdK;b>4cq+?h$(QTcutj?oN}F>H++E#@U+@;MD-ob^QGpl zkACpM4^}4C=ApcEBhayY`F847>h9>;xY~J24UFYo8%-_C{kI2i4X(VkI^7FUP-8y%-Ypq{xA6Els)&rBpz@!>D2SZxgm#1%Ex^<~AaBOW%?RZ9Q8O^&l z+Phc!KOVd@SQt9}=^3?WLT#Tca4j3&=4I}-^Okev)m5JwIHG!w=D95!(=M#H4Hesl zR-J0w$pRMuklpn@|1Q7IwHLYebuL)sf`#6v?{UK=&Z&2F6uFLduD8hbuFT%!4wd#i zjqrP1u(bE6Uh6Lj1A49Fk&SVB^11hy-d$Sv1dE=a>gZh|sv}eiKK1djJIB_8M~cBC zYnC;(5ImVb^Zw+!llPoLski^*$vczly(7imk+tAjZ=rY8=KZdh3y5 z>yb5=+WNh`hxQH^eZ#9)K1-{<6kq^l+K(68j~7myQ`^r23AZ?=sdJ-m;O@z_Znf`3 zskL{bt9K>3DydyZOFe@d-Fpg89fhv$6AxTg+Fd zh5j{s_)cW)Y-!Jt2M$}Sd#j!i`b)iirO@G0_fTmdygqQUIB;_9iaHPhgbw$E22X&` zJGXk7;BcwE|AEy~&u=jnJFf>MWit+p$uL+4eQtWV1{DZM5JsQf!jXtcG#Mr`&5Gu< zHDVDh)CGe%L^f@`*Z>e~1!4mT2ar1I&Y&^o(pDH}i=?;ZVx7f%)EfwOQ=zKV1ILR)kN># zcR;u*r50tO|1yN9G7(M+*D|1PYsZAbJV7H^wzXTMo4C=$-+zwJqtQ+DITG{q`FeB{ z9mR6re;SFiPewx)tvUMg#MIg7nX}JLjhz{LdE(-Q=+xNDXGQV~jL<9#$#jULYLKcn zjiZVeOpuYPK|8J4F>KY1tOo&6IC?IbO(nrzxY1tKv>i3)R?xpSbQ;xUS z=e=mDv!|xsoxcdEtsQxH$>F^@_uk1;hp;leYQ8hO8ZY)9D|Q^uU%2n@D*6ZRMvMLv zYrkIfpU*osJdHOGEjv_CZ-ML8Ik_B=!G;(Hk48E9ZGb)&S~FAVtZaLxYNepuR|zXj zlwV<X9O|_w|$kv9XmHDmvU{01X+B22FRDHdO9D}qC z_VDC1P(rim-JAc5E-D!Z5NU@Zp;PJp-)i>CWCm;$a^uD{;9f>?Ox3NjD3H&#fIAIc z)Gcx7C(wOCw{$h3XF;QvrPRFU!L~x(4IRM7!PQkzVNrS0g-wE{*1l8NsxH@Uz)=1U z3Myec&EOlVqrc>ByYFxP=*kCI*8Kx0BnR&vD)@);4ltap9m_L?(8)gw-hEv~8xMA& zK+XKmf}cvCdq0~|CtfMcTq;gnQcuqoTV}!f!;se>!ykC2&^fBMp8DCLPv7`F_}S~~ znFHLNUdKl7sI-!q&7UVwnQ&+5Uj#sU*VE>~ybgFDg>XQ3wn= z_Saxjko)Wg4z(*h$_&^E$5MhH*WF+NIBQOtOQw>!>u4e_Cv%W9Pw1wi#%&dV&*tYV zOevtP=-&?pBApR1kQo#XKqI+;Tny|@vB6YqV$@C=px6vzjbII8dWOJG7y=m!hM;Rm zGEkHmviDTTM9n~%<80I z-^v@ex>Of%q#qQzuj8(C$5U+D4_S2Y(Sq}+e)$!n4Xa_DUj6`F(ubH8+2yjQ#|>oz z4u(}UQI!Jf2AKeDF8o^XcL<_#=x1yCDIEH(m3~&V9Rq(}f=tj%g$?nATf|bU*3!0% zeN=tzhRg53^*lD0V|#zqQJMDB9h*hxW2@Cw$5&2OC%S5JBG@jUGUQ%*ogbG{G2O!{ zhg<>#FYr1llNI#%SwOEKE@aX>{GBTM&VZWK-w6#uW+INtY?dYBiAIWcEkwZY)w>gIZ4D1N9 zx9;ljI#<)|fE)IuR6}j84&yY0?Q~#bFoD*)li_#9rT+k!%HKc%JkK-!)^*>(qVM49 zN!9m4p1%)Hi$!&`-EZnz>Hgs2%FOD4JJEu7xZoT{A10`}I(F#-uCMP{v8kRt8{NGt zFWoiYo&Ln7b|3!0U25oD=~o*Dw!^=C4DY7UAaFG> zqZ%P6Jg3Ln%22pi=8d>ZWSC5se8F41*FwQvvbV zr4wh%=?rjo%1I&t6C3yn44ohF%&RgAheR8cAhM!B&MHhPQnm!IoT7FC=!232QiKi& zsR)e%+e5cPcPi$gp*2?t^H@&h0dy)TnKzg(ssC(U@!oY5`%e|S$BKc|s%N~w?ZQ$l zjG#2ojda9>(G5Ov%0`{FfnqHy>PYbMLd0aaooNW~rfO|K8OO*;_cGm#z;pp~p0so^ z^Cn=z;CLntxw)#d2wIL%`Al)CY~QxH>`vvJLt%TS)t(}phnRd*TLAmC+K zS~KV3kJE1!q1F7&wnW>1L9g;O6gQY(c-l(d<}KE04}96+{iy4AyH?uOhH(Bg*vxfr zu;>k{-oApf?|-(;CVSuuZ}Tsyy87+D{miQQX$!D9ayxY~FN9cvC-|iszr+BgOa=w` z8e>+o5t=q8e+-?VEK#CqHc;1-0c>8y;#DmE#Hhj4wB8DUkZspdRa{j8Ws{3b6CW%h+~1=Pw)x}iYxtAfQqy(8)1ZDf&a=;DBflsn5|asYq!~Y z^bt2&T=1&DJxfM+Z6Uu}4j2sD8uo)_^`KP6J;_z_$_e ziW6%#Lr&oT+n* Xf#RVBnp@cX#{ZJV6!@CKlCJ%KYvIZ4 literal 8159 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 Dict[str, Any]: - """Normalize Pi/FastF1-like telemetry payload to Enricher expected schema. + """Normalize lap-level telemetry payload from Pi stream to Enricher schema. - Accepted aliases: - - speed: Speed - - throttle: Throttle - - brake: Brake, Brakes - - tire_compound: Compound, TyreCompound, Tire - - fuel_level: Fuel, FuelRel, FuelLevel - - ers: ERS, ERSCharge - - track_temp: TrackTemp, track_temperature - - rain_probability: RainProb, PrecipProb - - lap: Lap, LapNumber, lap_number + Accepted aliases for lap-level data: + - lap_number: 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 + - lap_time: lap_time, LapTime, Time + - average_speed: average_speed, avg_speed, AvgSpeed + - max_speed: max_speed, MaxSpeed, max + - tire_compound: tire_compound, Compound, TyreCompound, Tire + - tire_life_laps: tire_life_laps, TireAge, tire_age + - track_temperature: track_temperature, TrackTemp, track_temp + - rainfall: rainfall, Rainfall, Rain - Values are clamped and defaulted if missing. + Returns normalized dict ready for enrichment layer. """ aliases = { - "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_temperature"], - "rain_probability": ["rain_probability", "RainProb", "PrecipProb"], + "lap_number": ["lap_number", "lap", "Lap", "LapNumber"], "total_laps": ["total_laps", "TotalLaps"], - "track_name": ["track_name", "TrackName", "Circuit"], - "driver_name": ["driver_name", "DriverName", "Driver"], - "current_position": ["current_position", "Position"], + "lap_time": ["lap_time", "LapTime", "Time"], + "average_speed": ["average_speed", "avg_speed", "AvgSpeed"], + "max_speed": ["max_speed", "MaxSpeed", "max"], + "tire_compound": ["tire_compound", "Compound", "TyreCompound", "Tire"], "tire_life_laps": ["tire_life_laps", "TireAge", "tire_age"], + "track_temperature": ["track_temperature", "TrackTemp", "track_temp"], "rainfall": ["rainfall", "Rainfall", "Rain"], } out: Dict[str, Any] = {} def pick(key: str, default=None): + """Pick first matching alias from payload.""" for k in aliases.get(key, [key]): if k in payload and payload[k] is not None: return payload[k] return default - def clamp01(x, default=0.0): - try: - v = float(x) - except (TypeError, ValueError): - return default - return max(0.0, min(1.0, v)) - - # Map values with sensible defaults - lap = pick("lap", 0) + # Extract and validate lap-level fields + lap_number = pick("lap_number", 0) try: - lap = int(lap) + lap_number = int(lap_number) except (TypeError, ValueError): - lap = 0 + lap_number = 0 - speed = pick("speed", 0.0) + total_laps = pick("total_laps", 51) try: - speed = float(speed) + total_laps = int(total_laps) except (TypeError, ValueError): - speed = 0.0 + total_laps = 51 - throttle = clamp01(pick("throttle", 0.0), 0.0) - brake = clamp01(pick("brake", 0.0), 0.0) + lap_time = pick("lap_time", None) + if lap_time: + out["lap_time"] = str(lap_time) + + average_speed = pick("average_speed", 0.0) + try: + average_speed = float(average_speed) + except (TypeError, ValueError): + average_speed = 0.0 + + max_speed = pick("max_speed", 0.0) + try: + max_speed = float(max_speed) + except (TypeError, ValueError): + max_speed = 0.0 tire_compound = pick("tire_compound", "medium") if isinstance(tire_compound, str): - tire_compound = tire_compound.lower() + tire_compound = tire_compound.upper() # Keep uppercase for consistency else: - tire_compound = "medium" + tire_compound = "MEDIUM" - fuel_level = clamp01(pick("fuel_level", 0.5), 0.5) - - ers = pick("ers", None) - if ers is not None: - ers = clamp01(ers, None) - - track_temp = pick("track_temp", None) + tire_life_laps = pick("tire_life_laps", 0) try: - track_temp = float(track_temp) if track_temp is not None else None + tire_life_laps = int(tire_life_laps) except (TypeError, ValueError): - track_temp = None + tire_life_laps = 0 - rain_prob = pick("rain_probability", None) + track_temperature = pick("track_temperature", 25.0) try: - rain_prob = clamp01(rain_prob, None) if rain_prob is not None else None - except Exception: - rain_prob = None + track_temperature = float(track_temperature) + except (TypeError, ValueError): + track_temperature = 25.0 + rainfall = pick("rainfall", False) + try: + rainfall = bool(rainfall) + except (TypeError, ValueError): + rainfall = False + + # Build normalized output out.update({ - "lap": lap, - "speed": speed, - "throttle": throttle, - "brake": brake, + "lap_number": lap_number, + "total_laps": total_laps, + "average_speed": average_speed, + "max_speed": max_speed, "tire_compound": tire_compound, - "fuel_level": fuel_level, + "tire_life_laps": tire_life_laps, + "track_temperature": track_temperature, + "rainfall": rainfall, }) - if ers is not None: - out["ers"] = ers - if track_temp is not None: - 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 diff --git a/hpcsim/api.py b/hpcsim/api.py index eb37ced..6cbd26d 100644 --- a/hpcsim/api.py +++ b/hpcsim/api.py @@ -25,24 +25,24 @@ _CALLBACK_URL = os.getenv("NEXT_STAGE_CALLBACK_URL") class EnrichedRecord(BaseModel): + """Lap-level enriched telemetry model.""" lap: int - aero_efficiency: float - tire_degradation_index: float - ers_charge: float - fuel_optimization_score: float - driver_consistency: float - weather_impact: str + tire_degradation_rate: float + pace_trend: str + tire_cliff_risk: float + optimal_pit_window: List[int] + performance_delta: float @app.post("/ingest/telemetry") async def ingest_telemetry(payload: Dict[str, Any] = Body(...)): - """Receive raw telemetry (from Pi), normalize, enrich, return enriched with race context. + """Receive raw lap-level telemetry (from Pi), normalize, enrich, return enriched with race context. Optionally forwards to NEXT_STAGE_CALLBACK_URL if set. """ try: normalized = normalize_telemetry(payload) - result = _enricher.enrich_with_context(normalized) + result = _enricher.enrich_lap_data(normalized) enriched = result["enriched_telemetry"] race_context = result["race_context"] except Exception as e: @@ -85,3 +85,12 @@ async def list_enriched(limit: int = 50): @app.get("/healthz") async def healthz(): return {"status": "ok", "stored": len(_recent)} + + +@app.post("/reset") +async def reset_enricher(): + """Reset enricher state for a new session/race.""" + global _enricher + _enricher = Enricher() + _recent.clear() + return {"status": "reset", "message": "Enricher state and buffer cleared"} diff --git a/hpcsim/enrichment.py b/hpcsim/enrichment.py index 4c802bb..04885a1 100644 --- a/hpcsim/enrichment.py +++ b/hpcsim/enrichment.py @@ -2,370 +2,254 @@ from __future__ import annotations from dataclasses import dataclass, field from typing import Dict, Any, Optional, List -import math +import pandas as pd -# --- Contracts --- -# Input telemetry (example, extensible): +# --- LAP-LEVEL TELEMETRY CONTRACT --- +# Input from Raspberry Pi (lap-level data): # { -# "lap": 27, -# "speed": 282, # km/h -# "throttle": 0.91, # 0..1 -# "brake": 0.05, # 0..1 -# "tire_compound": "medium",# soft|medium|hard|inter|wet -# "fuel_level": 0.47, # 0..1 (fraction of race fuel) -# "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 + race context: -# { -# "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": [...] -# } +# "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 # } -_TIRES_BASE_WEAR = { - "soft": 0.012, - "medium": 0.008, - "hard": 0.006, - "inter": 0.015, - "wet": 0.02, +_TIRE_DEGRADATION_RATES = { + "soft": 0.030, # Fast degradation + "medium": 0.020, # Moderate degradation + "hard": 0.015, # Slow degradation + "inter": 0.025, + "wet": 0.022, } +_TIRE_CLIFF_THRESHOLD = 25 # Laps before cliff risk increases significantly + @dataclass class EnricherState: - last_lap: Optional[int] = None - 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) + """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" class Enricher: - """Heuristic enrichment engine to simulate HPC analytics on telemetry. - - Stateless inputs are enriched with stateful estimates (wear, consistency, etc.). - Designed for predictable, dependency-free behavior. + """ + HPC-simulated enrichment for lap-level F1 telemetry. + + Accepts lap-level data from Raspberry Pi and generates performance insights + that simulate HPC computational analysis. """ def __init__(self): self.state = EnricherState() + self._baseline_lap_time: Optional[float] = None # Best lap time as baseline - # --- 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)) - 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") - rain_prob = telemetry.get("rain_probability") - - # Update per-lap aggregates - self._update_lap_stats(lap, speed, throttle) - - # 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) - - return { - "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, - } - - 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) + 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)) - # 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)) + # 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) - # 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, + "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) } # 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 - ) + 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) + } + } 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 + # --- HPC-Simulated Computation Methods --- - 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 = [] + 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) - # 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" - ] + # 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 - tire_compounds = ["soft", "medium", "hard"] + # 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 - # 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) + degradation = base_rate * tire_age * temp_multiplier * age_multiplier + return min(1.0, degradation) + + 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" - 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) + recent_laps = self.state.lap_times[-5:] # Last 5 laps - return competitors - - # --- Internals --- - def _update_lap_stats(self, lap: int, speed: float, throttle: float) -> None: - if lap <= 0: - return - # Store simple aggregates for consistency metrics - self.state.lap_speeds[lap] = speed - self.state.lap_throttle_avg[lap] = 0.8 * self.state.lap_throttle_avg.get(lap, throttle) + 0.2 * throttle - self.state.last_lap = lap - - def _compute_aero_efficiency(self, speed: float, throttle: float, brake: float) -> float: - # Heuristic: favor high speed with low throttle variance (efficiency) and minimal braking at high speeds - # Normalize speed into 0..1 assuming 0..330 km/h typical - speed_n = max(0.0, min(1.0, speed / 330.0)) - brake_penalty = 0.4 * brake - throttle_bonus = 0.2 * throttle - base = 0.5 * speed_n + throttle_bonus - brake_penalty - return max(0.0, min(1.0, base)) - - def _compute_tire_degradation(self, lap: int, speed: float, throttle: float, tire_compound: str, track_temp: Optional[float]) -> float: - base_wear = _TIRES_BASE_WEAR.get(tire_compound, _TIRES_BASE_WEAR["medium"]) # per lap - temp_factor = 1.0 - if isinstance(track_temp, (int, float)): - if track_temp > 42: - temp_factor = 1.25 - elif track_temp < 15: - temp_factor = 0.9 - stress = 0.5 + 0.5 * throttle + 0.2 * max(0.0, (speed - 250.0) / 100.0) - wear_this_lap = base_wear * stress * temp_factor - # Update cumulative wear but cap at 1.0 - self.state.cumulative_wear = min(1.0, self.state.cumulative_wear + wear_this_lap) - return self.state.cumulative_wear - - def _compute_ers_charge(self, ers: Optional[float], throttle: float, brake: float) -> float: - if isinstance(ers, (int, float)): - # simple recovery under braking, depletion under throttle - ers_level = float(ers) + 0.1 * brake - 0.05 * throttle + # 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) + + diff = avg_second_half - avg_first_half + + 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: - # infer ers trend if not provided - ers_level = 0.6 + 0.05 * brake - 0.03 * throttle - return max(0.0, min(1.0, ers_level)) - - def _compute_fuel_optimization(self, fuel_level: float, throttle: float) -> float: - # Reward keeping throttle moderate when fuel is low and pushing when fuel is high - fuel_n = max(0.0, min(1.0, fuel_level)) - ideal_throttle = 0.5 + 0.4 * fuel_n # higher fuel -> higher ideal throttle - penalty = abs(throttle - ideal_throttle) - score = 1.0 - penalty - return max(0.0, min(1.0, score)) - - def _compute_driver_consistency(self) -> float: - # Use last up to 5 laps speed variance to estimate consistency (lower variance -> higher consistency) - laps = sorted(self.state.lap_speeds.keys())[-5:] - if not laps: - return 0.5 - speeds = [self.state.lap_speeds[l] for l in laps] - mean = sum(speeds) / len(speeds) - var = sum((s - mean) ** 2 for s in speeds) / len(speeds) - # Map variance to 0..1; assume 0..(30 km/h)^2 typical range - norm = min(1.0, var / (30.0 ** 2)) - return max(0.0, 1.0 - norm) - - def _compute_weather_impact(self, rain_prob: Optional[float], track_temp: Optional[float]) -> str: - score = 0.0 - if isinstance(rain_prob, (int, float)): - score += 0.7 * float(rain_prob) - if isinstance(track_temp, (int, float)): - if track_temp < 12: # cold tires harder - score += 0.2 - if track_temp > 45: # overheating - score += 0.2 - if score < 0.3: - return "low" - if score < 0.6: - return "medium" - return "high" + 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 + } + + 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 + 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 diff --git a/requirements.txt b/requirements.txt index fbc9097..1175ddd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,11 @@ fastapi==0.115.2 uvicorn==0.31.1 httpx==0.27.2 +aiohttp==3.10.10 elevenlabs==2.18.0 python-dotenv==1.1.1 fastf1==3.6.1 pandas==2.3.3 -requests==2.32.5 \ No newline at end of file +requests==2.32.5 +websockets==13.1 +pydantic==2.9.2 \ No newline at end of file diff --git a/scripts/simulate_pi_stream.py b/scripts/simulate_pi_stream.py index 5ffa52b..c10477a 100644 --- a/scripts/simulate_pi_stream.py +++ b/scripts/simulate_pi_stream.py @@ -1,13 +1,14 @@ """ -Raspberry Pi Telemetry Stream Simulator +Raspberry Pi Telemetry Stream Simulator - Lap-Level Data -Reads the ALONSO_2023_MONZA_RACE CSV file row by row and simulates +Reads the ALONSO_2023_MONZA_LAPS.csv file lap by lap and simulates live telemetry streaming from a Raspberry Pi sensor. -Sends data to the HPC simulation layer via HTTP POST at intervals -determined by the time differences between consecutive rows. +Sends data to the HPC simulation layer via HTTP POST at fixed +1-minute intervals between laps. Usage: - python simulate_pi_stream.py --data ALONSO_2023_MONZA_RACE --speed 1.0 + python simulate_pi_stream.py + python simulate_pi_stream.py --interval 30 # 30 seconds between laps """ import argparse @@ -19,39 +20,32 @@ import pandas as pd import requests -def load_telemetry_csv(filepath: Path) -> pd.DataFrame: - """Load telemetry data from CSV file.""" - df = pd.read_csv(filepath, index_col=0) +def load_lap_csv(filepath: Path) -> pd.DataFrame: + """Load lap-level telemetry data from CSV file.""" + df = pd.read_csv(filepath) - # Convert overall_time to timedelta if it's not already - if df['overall_time'].dtype == 'object': - df['overall_time'] = pd.to_timedelta(df['overall_time']) + # Convert lap_time to timedelta if it's not already + if 'lap_time' in df.columns and df['lap_time'].dtype == 'object': + df['lap_time'] = pd.to_timedelta(df['lap_time']) - print(f"✓ Loaded {len(df)} telemetry points from {filepath}") + print(f"✓ Loaded {len(df)} laps from {filepath}") print(f" Laps: {df['lap_number'].min():.0f} → {df['lap_number'].max():.0f}") - print(f" Duration: {df['overall_time'].iloc[-1]}") return df -def row_to_json(row: pd.Series) -> Dict[str, Any]: - """Convert a DataFrame row to a JSON-compatible dictionary.""" +def lap_to_json(row: pd.Series) -> Dict[str, Any]: + """Convert a lap DataFrame row to a JSON-compatible dictionary.""" data = { 'lap_number': int(row['lap_number']) if pd.notna(row['lap_number']) else None, 'total_laps': int(row['total_laps']) if pd.notna(row['total_laps']) else None, - 'speed': float(row['speed']) if pd.notna(row['speed']) else 0.0, - 'throttle': float(row['throttle']) if pd.notna(row['throttle']) else 0.0, - 'brake': bool(row['brake']), + 'lap_time': str(row['lap_time']) if pd.notna(row['lap_time']) else None, + 'average_speed': float(row['average_speed']) if pd.notna(row['average_speed']) else 0.0, + 'max_speed': float(row['max_speed']) if pd.notna(row['max_speed']) else 0.0, 'tire_compound': str(row['tire_compound']) if pd.notna(row['tire_compound']) else 'UNKNOWN', - 'tire_life_laps': float(row['tire_life_laps']) if pd.notna(row['tire_life_laps']) else 0.0, + 'tire_life_laps': int(row['tire_life_laps']) if pd.notna(row['tire_life_laps']) else 0, 'track_temperature': float(row['track_temperature']) if pd.notna(row['track_temperature']) else 0.0, - 'rainfall': bool(row['rainfall']), - - # Additional race context fields - 'track_name': 'Monza', # From ALONSO_2023_MONZA_RACE - 'driver_name': 'Alonso', - 'current_position': 5, # Mock position, could be varied - 'fuel_level': max(0.1, 1.0 - (float(row['lap_number']) / float(row['total_laps']) * 0.8)) if pd.notna(row['lap_number']) and pd.notna(row['total_laps']) else 0.5, + 'rainfall': bool(row['rainfall']) } return data @@ -59,17 +53,17 @@ def row_to_json(row: pd.Series) -> Dict[str, Any]: def simulate_stream( df: pd.DataFrame, endpoint: str, - speed: float = 1.0, + interval: int = 60, start_lap: int = 1, end_lap: int = None ): """ - Simulate live telemetry streaming based on actual time intervals in the data. + Simulate live telemetry streaming with fixed interval between laps. Args: - df: DataFrame with telemetry data + df: DataFrame with lap-level telemetry data endpoint: HPC API endpoint URL - speed: Playback speed multiplier (1.0 = real-time, 2.0 = 2x speed) + interval: Fixed interval in seconds between laps (default: 60 seconds) start_lap: Starting lap number end_lap: Ending lap number (None = all laps) """ @@ -79,95 +73,84 @@ def simulate_stream( filtered_df = filtered_df[filtered_df['lap_number'] <= end_lap].copy() if len(filtered_df) == 0: - print("❌ No telemetry points in specified lap range") + print("❌ No laps in specified lap range") return # Reset index for easier iteration filtered_df = filtered_df.reset_index(drop=True) - print(f"\n🏁 Starting telemetry stream simulation") + print(f"\n🏁 Starting lap-level telemetry stream simulation") print(f" Endpoint: {endpoint}") print(f" Laps: {start_lap} → {end_lap or 'end'}") - print(f" Speed: {speed}x") - print(f" Points: {len(filtered_df)}") - - total_duration = (filtered_df['overall_time'].iloc[-1] - filtered_df['overall_time'].iloc[0]).total_seconds() - print(f" Duration: {total_duration:.1f}s (real-time) → {total_duration / speed:.1f}s (playback)\n") + print(f" Interval: {interval} seconds between laps") + print(f" Total laps: {len(filtered_df)}") + print(f" Est. duration: {len(filtered_df) * interval / 60:.1f} minutes\n") sent_count = 0 error_count = 0 - current_lap = start_lap try: for i in range(len(filtered_df)): row = filtered_df.iloc[i] + lap_num = int(row['lap_number']) - # Calculate sleep time based on time difference to next row - if i < len(filtered_df) - 1: - next_row = filtered_df.iloc[i + 1] - time_diff = (next_row['overall_time'] - row['overall_time']).total_seconds() - sleep_time = time_diff / speed - - # Ensure positive sleep time - if sleep_time < 0: - sleep_time = 0 - else: - sleep_time = 0 + # Convert lap to JSON + lap_data = lap_to_json(row) - # Convert row to JSON - telemetry_point = row_to_json(row) - - # Send telemetry point + # Send lap data try: response = requests.post( endpoint, - json=telemetry_point, - timeout=2.0 + json=lap_data, + timeout=5.0 ) if response.status_code == 200: sent_count += 1 + progress = (i + 1) / len(filtered_df) * 100 - # Print progress updates - if row['lap_number'] > current_lap: - current_lap = row['lap_number'] - progress = (i + 1) / len(filtered_df) * 100 - print(f" 📡 Lap {int(current_lap)}: {sent_count} points sent " - f"({progress:.1f}% complete)") - elif sent_count % 500 == 0: - progress = (i + 1) / len(filtered_df) * 100 - print(f" 📡 Lap {int(row['lap_number'])}: {sent_count} points sent " - f"({progress:.1f}% complete)") + # Print lap info + print(f" 📡 Lap {lap_num}/{int(row['total_laps'])}: " + f"Avg Speed: {row['average_speed']:.1f} km/h, " + f"Tire: {row['tire_compound']} (age: {int(row['tire_life_laps'])} laps) " + f"[{progress:.0f}%]") + + # Show response if it contains strategies + try: + response_data = response.json() + if 'strategies_generated' in response_data: + print(f" ✓ Generated {response_data['strategies_generated']} strategies") + except: + pass else: error_count += 1 - print(f" ⚠ HTTP {response.status_code}: {response.text[:50]}") + print(f" ⚠ Lap {lap_num}: HTTP {response.status_code}: {response.text[:100]}") except requests.RequestException as e: error_count += 1 - if error_count % 10 == 0: - print(f" ⚠ Connection error ({error_count} total): {str(e)[:50]}") + print(f" ⚠ Lap {lap_num}: Connection error: {str(e)[:100]}") - # Sleep until next point should be sent - if sleep_time > 0: - time.sleep(sleep_time) + # Sleep for fixed interval before next lap (except for last lap) + if i < len(filtered_df) - 1: + time.sleep(interval) print(f"\n✅ Stream complete!") - print(f" Sent: {sent_count} points") + print(f" Sent: {sent_count} laps") print(f" Errors: {error_count}") except KeyboardInterrupt: print(f"\n⏸ Stream interrupted by user") - print(f" Sent: {sent_count}/{len(filtered_df)} points") + print(f" Sent: {sent_count}/{len(filtered_df)} laps") def main(): parser = argparse.ArgumentParser( - description="Simulate Raspberry Pi telemetry streaming from CSV data" + description="Simulate Raspberry Pi lap-level telemetry streaming" ) - parser.add_argument("--endpoint", type=str, default="http://localhost:8000/telemetry", - help="HPC API endpoint") - parser.add_argument("--speed", type=float, default=1.0, - help="Playback speed (1.0 = real-time, 10.0 = 10x speed)") + parser.add_argument("--endpoint", type=str, default="http://localhost:8000/ingest/telemetry", + help="HPC API endpoint (default: http://localhost:8000/ingest/telemetry)") + parser.add_argument("--interval", type=int, default=60, + help="Fixed interval in seconds between laps (default: 60)") parser.add_argument("--start-lap", type=int, default=1, help="Starting lap number") parser.add_argument("--end-lap", type=int, default=None, help="Ending lap number") @@ -176,19 +159,19 @@ def main(): try: # Hardcoded CSV file location in the same folder as this script script_dir = Path(__file__).parent - data_path = script_dir / "ALONSO_2023_MONZA_RACE.csv" + data_path = script_dir / "ALONSO_2023_MONZA_LAPS.csv" - df = load_telemetry_csv(data_path) + df = load_lap_csv(data_path) simulate_stream( df, args.endpoint, - args.speed, + args.interval, args.start_lap, args.end_lap ) except FileNotFoundError: print(f"❌ File not found: {data_path}") - print(f" Make sure ALONSO_2023_MONZA_RACE.csv is in the scripts/ folder") + print(f" Make sure ALONSO_2023_MONZA_LAPS.csv is in the scripts/ folder") sys.exit(1) except Exception as e: print(f"❌ Error: {e}") diff --git a/scripts/simulate_pi_websocket.py b/scripts/simulate_pi_websocket.py new file mode 100644 index 0000000..36a23c4 --- /dev/null +++ b/scripts/simulate_pi_websocket.py @@ -0,0 +1,403 @@ +#!/usr/bin/env python3 +""" +WebSocket-based Raspberry Pi Telemetry Simulator. + +Connects to AI Intelligence Layer via WebSocket and: +1. Streams lap telemetry to AI layer +2. Receives control commands (brake_bias, differential_slip) from AI layer +3. Applies control adjustments in real-time + +Usage: + python simulate_pi_websocket.py --interval 5 --ws-url ws://localhost:9000/ws/pi +""" +from __future__ import annotations + +import argparse +import asyncio +import json +import logging +from pathlib import Path +from typing import Dict, Any, Optional +import sys + +try: + import pandas as pd + import websockets + from websockets.client import WebSocketClientProtocol +except ImportError: + print("Error: Required packages not installed.") + print("Run: pip install pandas websockets") + sys.exit(1) + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + + +class PiSimulator: + """WebSocket-based Pi simulator with control feedback.""" + + def __init__(self, csv_path: Path, ws_url: str, interval: float = 60.0, enrichment_url: str = "http://localhost:8000"): + self.csv_path = csv_path + self.ws_url = ws_url + self.enrichment_url = enrichment_url + self.interval = interval + self.df: Optional[pd.DataFrame] = None + self.current_controls = { + "brake_bias": 5, + "differential_slip": 5 + } + + def load_lap_csv(self) -> pd.DataFrame: + """Load lap-level CSV data.""" + logger.info(f"Loading CSV from {self.csv_path}") + df = pd.read_csv(self.csv_path) + logger.info(f"Loaded {len(df)} laps") + return df + + def lap_to_raw_payload(self, row: pd.Series) -> Dict[str, Any]: + """ + Convert CSV row to raw lap telemetry (for enrichment service). + This is what the real Pi would send. + """ + return { + "lap_number": int(row["lap_number"]), + "total_laps": int(row["total_laps"]), + "lap_time": str(row["lap_time"]), + "average_speed": float(row["average_speed"]), + "max_speed": float(row["max_speed"]), + "tire_compound": str(row["tire_compound"]), + "tire_life_laps": int(row["tire_life_laps"]), + "track_temperature": float(row["track_temperature"]), + "rainfall": bool(row.get("rainfall", False)) + } + + async def enrich_telemetry(self, raw_telemetry: Dict[str, Any]) -> Dict[str, Any]: + """ + Send raw telemetry to enrichment service and get back enriched data. + This simulates the Pi → Enrichment → AI flow. + """ + import aiohttp + + try: + async with aiohttp.ClientSession() as session: + async with session.post( + f"{self.enrichment_url}/ingest/telemetry", + json=raw_telemetry, + timeout=aiohttp.ClientTimeout(total=5.0) + ) as response: + if response.status == 200: + result = await response.json() + logger.info(f" ✓ Enrichment service processed lap {raw_telemetry['lap_number']}") + return result + else: + logger.error(f" ✗ Enrichment service error: {response.status}") + return None + except Exception as e: + logger.error(f" ✗ Failed to connect to enrichment service: {e}") + logger.error(f" Make sure enrichment service is running: python scripts/serve.py") + return None + + def lap_to_enriched_payload(self, row: pd.Series) -> Dict[str, Any]: + """ + Convert CSV row to enriched telemetry payload. + Simulates the enrichment layer output. + """ + # Basic enrichment simulation (would normally come from enrichment service) + lap_number = int(row["lap_number"]) + tire_age = int(row["tire_life_laps"]) + + # Simple tire degradation simulation + tire_deg_rate = min(1.0, 0.02 * tire_age) + tire_cliff_risk = max(0.0, min(1.0, (tire_age - 20) / 10.0)) + + # Pace trend (simplified) + pace_trend = "stable" + if tire_age > 25: + pace_trend = "declining" + elif tire_age < 5: + pace_trend = "improving" + + # Optimal pit window + if tire_age > 20: + pit_window = [lap_number + 1, lap_number + 3] + else: + pit_window = [lap_number + 10, lap_number + 15] + + # Performance delta (random for simulation) + import random + performance_delta = random.uniform(-1.5, 1.0) + + 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) + } + + race_context = { + "race_info": { + "track_name": "Monza", + "total_laps": int(row["total_laps"]), + "current_lap": lap_number, + "weather_condition": "Wet" if row.get("rainfall", False) else "Dry", + "track_temp_celsius": float(row["track_temperature"]) + }, + "driver_state": { + "driver_name": "Alonso", + "current_position": 5, + "current_tire_compound": str(row["tire_compound"]).lower(), + "tire_age_laps": tire_age, + "fuel_remaining_percent": max(0.0, 100.0 * (1.0 - (lap_number / int(row["total_laps"])))) + }, + "competitors": [] + } + + return { + "type": "telemetry", + "lap_number": lap_number, + "enriched_telemetry": enriched_telemetry, + "race_context": race_context + } + + async def stream_telemetry(self): + """Main WebSocket streaming loop.""" + self.df = self.load_lap_csv() + + # Reset enrichment service state for fresh session + logger.info(f"Resetting enrichment service state...") + try: + import aiohttp + async with aiohttp.ClientSession() as session: + async with session.post( + f"{self.enrichment_url}/reset", + timeout=aiohttp.ClientTimeout(total=5.0) + ) as response: + if response.status == 200: + logger.info("✓ Enrichment service reset successfully") + else: + logger.warning(f"⚠ Enrichment reset returned status {response.status}") + except Exception as e: + logger.warning(f"⚠ Could not reset enrichment service: {e}") + logger.warning(" Continuing anyway (enricher may have stale state)") + + logger.info(f"Connecting to WebSocket: {self.ws_url}") + + try: + async with websockets.connect(self.ws_url) as websocket: + logger.info("WebSocket connected!") + + # Wait for welcome message + welcome = await websocket.recv() + logger.info(f"Received: {welcome}") + + # Stream each lap + for idx, row in self.df.iterrows(): + lap_number = int(row["lap_number"]) + + logger.info(f"\n{'='*60}") + logger.info(f"Lap {lap_number}/{int(row['total_laps'])}") + logger.info(f"{'='*60}") + + # Build raw telemetry payload (what real Pi would send) + raw_telemetry = self.lap_to_raw_payload(row) + logger.info(f"[RAW] Lap {lap_number} telemetry prepared") + + # Send to enrichment service for processing + enriched_data = await self.enrich_telemetry(raw_telemetry) + + if not enriched_data: + logger.error("Failed to get enrichment, skipping lap") + await asyncio.sleep(self.interval) + continue + + # Extract enriched telemetry and race context from enrichment service + enriched_telemetry = enriched_data.get("enriched_telemetry") + race_context = enriched_data.get("race_context") + + if not enriched_telemetry or not race_context: + logger.error("Invalid enrichment response, skipping lap") + await asyncio.sleep(self.interval) + continue + + # Build WebSocket payload for AI layer + ws_payload = { + "type": "telemetry", + "lap_number": lap_number, + "enriched_telemetry": enriched_telemetry, + "race_context": race_context + } + + # Send enriched telemetry to AI layer via WebSocket + await websocket.send(json.dumps(ws_payload)) + logger.info(f"[SENT] Lap {lap_number} enriched telemetry to AI layer") + + # Wait for control command response(s) + try: + response = await asyncio.wait_for(websocket.recv(), timeout=5.0) + response_data = json.loads(response) + + if response_data.get("type") == "control_command": + brake_bias = response_data.get("brake_bias", 5) + diff_slip = response_data.get("differential_slip", 5) + strategy_name = response_data.get("strategy_name", "N/A") + message = response_data.get("message") + + self.current_controls["brake_bias"] = brake_bias + self.current_controls["differential_slip"] = diff_slip + + logger.info(f"[RECEIVED] Control Command:") + logger.info(f" ├─ Brake Bias: {brake_bias}/10") + logger.info(f" ├─ Differential Slip: {diff_slip}/10") + if strategy_name != "N/A": + logger.info(f" └─ Strategy: {strategy_name}") + if message: + logger.info(f" └─ {message}") + + # Apply controls (in real Pi, this would adjust hardware) + self.apply_controls(brake_bias, diff_slip) + + # If message indicates processing, wait for update + if message and "Processing" in message: + logger.info(" AI is generating strategies, waiting for update...") + try: + update = await asyncio.wait_for(websocket.recv(), timeout=45.0) + update_data = json.loads(update) + + if update_data.get("type") == "control_command_update": + brake_bias = update_data.get("brake_bias", 5) + diff_slip = update_data.get("differential_slip", 5) + strategy_name = update_data.get("strategy_name", "N/A") + + self.current_controls["brake_bias"] = brake_bias + self.current_controls["differential_slip"] = diff_slip + + logger.info(f"[UPDATED] Strategy-Based Control:") + logger.info(f" ├─ Brake Bias: {brake_bias}/10") + logger.info(f" ├─ Differential Slip: {diff_slip}/10") + logger.info(f" └─ Strategy: {strategy_name}") + + self.apply_controls(brake_bias, diff_slip) + except asyncio.TimeoutError: + logger.warning("[TIMEOUT] Strategy generation took too long") + + elif response_data.get("type") == "error": + logger.error(f"[ERROR] {response_data.get('message')}") + + except asyncio.TimeoutError: + logger.warning("[TIMEOUT] No control command received within 5s") + + # Wait before next lap + logger.info(f"Waiting {self.interval}s before next lap...") + await asyncio.sleep(self.interval) + + # All laps complete + logger.info("\n" + "="*60) + logger.info("RACE COMPLETE - All laps streamed") + logger.info("="*60) + + # Send disconnect message + await websocket.send(json.dumps({"type": "disconnect"})) + + except websockets.exceptions.WebSocketException as e: + logger.error(f"WebSocket error: {e}") + logger.error("Is the AI Intelligence Layer running on port 9000?") + except Exception as e: + logger.error(f"Unexpected error: {e}") + + def apply_controls(self, brake_bias: int, differential_slip: int): + """ + Apply control adjustments to the car. + In real Pi, this would interface with hardware controllers. + """ + logger.info(f"[APPLYING] Setting brake_bias={brake_bias}, diff_slip={differential_slip}") + + # Simulate applying controls (in real implementation, this would: + # - Adjust brake bias actuator + # - Modify differential slip controller + # - Send CAN bus messages to ECU + # - Update dashboard display) + + # For simulation, just log the change + if brake_bias > 6: + logger.info(" → Brake bias shifted REAR (protecting front tires)") + elif brake_bias < 5: + logger.info(" → Brake bias shifted FRONT (aggressive turn-in)") + else: + logger.info(" → Brake bias NEUTRAL") + + if differential_slip > 6: + logger.info(" → Differential slip INCREASED (gentler on tires)") + elif differential_slip < 5: + logger.info(" → Differential slip DECREASED (aggressive cornering)") + else: + logger.info(" → Differential slip NEUTRAL") + + +async def main(): + parser = argparse.ArgumentParser( + description="WebSocket-based Raspberry Pi Telemetry Simulator" + ) + parser.add_argument( + "--interval", + type=float, + default=60.0, + help="Seconds between laps (default: 60s)" + ) + parser.add_argument( + "--ws-url", + type=str, + default="ws://localhost:9000/ws/pi", + help="WebSocket URL for AI layer (default: ws://localhost:9000/ws/pi)" + ) + parser.add_argument( + "--enrichment-url", + type=str, + default="http://localhost:8000", + help="Enrichment service URL (default: http://localhost:8000)" + ) + parser.add_argument( + "--csv", + type=str, + default=None, + help="Path to lap CSV file (default: scripts/ALONSO_2023_MONZA_LAPS.csv)" + ) + + args = parser.parse_args() + + # Determine CSV path + if args.csv: + csv_path = Path(args.csv) + else: + script_dir = Path(__file__).parent + csv_path = script_dir / "ALONSO_2023_MONZA_LAPS.csv" + + if not csv_path.exists(): + logger.error(f"CSV file not found: {csv_path}") + sys.exit(1) + + # Create simulator and run + simulator = PiSimulator( + csv_path=csv_path, + ws_url=args.ws_url, + enrichment_url=args.enrichment_url, + interval=args.interval + ) + + logger.info("Starting WebSocket Pi Simulator") + logger.info(f"CSV: {csv_path}") + logger.info(f"Enrichment Service: {args.enrichment_url}") + logger.info(f"WebSocket URL: {args.ws_url}") + logger.info(f"Interval: {args.interval}s per lap") + logger.info("-" * 60) + + await simulator.stream_telemetry() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/scripts/test_websocket.py b/scripts/test_websocket.py new file mode 100644 index 0000000..3e74b04 --- /dev/null +++ b/scripts/test_websocket.py @@ -0,0 +1,157 @@ +#!/usr/bin/env python3 +""" +Quick test to verify WebSocket control system. +Tests the complete flow: Pi → AI → Control Commands +""" +import asyncio +import json +import sys + +try: + import websockets +except ImportError: + print("Error: websockets not installed") + print("Run: pip install websockets") + sys.exit(1) + + +async def test_websocket(): + """Test WebSocket connection and control flow.""" + + ws_url = "ws://localhost:9000/ws/pi" + print(f"Testing WebSocket connection to {ws_url}") + print("-" * 60) + + try: + async with websockets.connect(ws_url) as websocket: + print("✓ WebSocket connected!") + + # 1. Receive welcome message + welcome = await websocket.recv() + welcome_data = json.loads(welcome) + print(f"✓ Welcome message: {welcome_data.get('message')}") + + # 2. Send test telemetry (lap 1) + test_payload = { + "type": "telemetry", + "lap_number": 1, + "enriched_telemetry": { + "lap": 1, + "tire_degradation_rate": 0.15, + "pace_trend": "stable", + "tire_cliff_risk": 0.05, + "optimal_pit_window": [25, 30], + "performance_delta": 0.0 + }, + "race_context": { + "race_info": { + "track_name": "Monza", + "total_laps": 51, + "current_lap": 1, + "weather_condition": "Dry", + "track_temp_celsius": 28.0 + }, + "driver_state": { + "driver_name": "Test Driver", + "current_position": 5, + "current_tire_compound": "medium", + "tire_age_laps": 1, + "fuel_remaining_percent": 98.0 + }, + "competitors": [] + } + } + + print("\n→ Sending lap 1 telemetry...") + await websocket.send(json.dumps(test_payload)) + + # 3. Wait for response (short timeout for first laps) + response = await asyncio.wait_for(websocket.recv(), timeout=5.0) + response_data = json.loads(response) + + if response_data.get("type") == "control_command": + print("✓ Received control command!") + print(f" Brake Bias: {response_data.get('brake_bias')}/10") + print(f" Differential Slip: {response_data.get('differential_slip')}/10") + print(f" Message: {response_data.get('message', 'N/A')}") + else: + print(f"✗ Unexpected response: {response_data}") + + # 4. Send two more laps to trigger strategy generation + for lap_num in [2, 3]: + test_payload["lap_number"] = lap_num + test_payload["enriched_telemetry"]["lap"] = lap_num + test_payload["race_context"]["race_info"]["current_lap"] = lap_num + + print(f"\n→ Sending lap {lap_num} telemetry...") + await websocket.send(json.dumps(test_payload)) + + # Lap 3 triggers Gemini, so expect two responses + if lap_num == 3: + print(f" (lap 3 will trigger strategy generation - may take 10-30s)") + + # First response: immediate acknowledgment + response1 = await asyncio.wait_for(websocket.recv(), timeout=5.0) + response1_data = json.loads(response1) + print(f"✓ Immediate response: {response1_data.get('message', 'Processing...')}") + + # Second response: strategy-based controls + print(" Waiting for strategy generation to complete...") + response2 = await asyncio.wait_for(websocket.recv(), timeout=45.0) + response2_data = json.loads(response2) + + if response2_data.get("type") == "control_command_update": + print(f"✓ Lap {lap_num} strategy-based control received!") + print(f" Brake Bias: {response2_data.get('brake_bias')}/10") + print(f" Differential Slip: {response2_data.get('differential_slip')}/10") + + strategy = response2_data.get('strategy_name') + if strategy and strategy != "N/A": + print(f" Strategy: {strategy}") + print(f" Total Strategies: {response2_data.get('total_strategies')}") + print("✓ Strategy generation successful!") + else: + print(f"✗ Unexpected response: {response2_data}") + else: + # Laps 1-2: just one response + response = await asyncio.wait_for(websocket.recv(), timeout=5.0) + response_data = json.loads(response) + + if response_data.get("type") == "control_command": + print(f"✓ Lap {lap_num} control command received!") + print(f" Brake Bias: {response_data.get('brake_bias')}/10") + print(f" Differential Slip: {response_data.get('differential_slip')}/10") + + # 5. Disconnect + print("\n→ Sending disconnect...") + await websocket.send(json.dumps({"type": "disconnect"})) + + print("\n" + "=" * 60) + print("✓ ALL TESTS PASSED!") + print("=" * 60) + print("\nWebSocket control system is working correctly.") + print("Ready to run: python scripts/simulate_pi_websocket.py") + + except websockets.exceptions.WebSocketException as e: + print(f"\n✗ WebSocket error: {e}") + print("\nMake sure the AI Intelligence Layer is running:") + print(" cd ai_intelligence_layer && python main.py") + sys.exit(1) + + except asyncio.TimeoutError: + print("\n✗ Timeout waiting for response") + print("AI layer may be processing (Gemini API can be slow)") + print("Check ai_intelligence_layer logs for details") + sys.exit(1) + + except Exception as e: + print(f"\n✗ Unexpected error: {e}") + import traceback + traceback.print_exc() + sys.exit(1) + + +if __name__ == "__main__": + print("WebSocket Control System Test") + print("=" * 60) + asyncio.run(test_websocket())