""" HTML Report Generator v3.0 โ Enhanced with interactive charts and comprehensive analysis. Generates detailed race prediction reports with: - Driver probability distributions - Feature breakdown charts - Podium predictions - Tire strategy recommendations - Weather impact analysis """ import os import json import logging from typing import Optional, Dict from datetime import datetime logger = logging.getLogger(__name__) def generate_report( circuit_id: str, rain_probability: Optional[float] = None, n_simulations: int = 10000, output_path: Optional[str] = None, ) -> str: """ Generate comprehensive HTML race prediction report. Args: circuit_id: Circuit identifier rain_probability: Rain probability (0.0-1.0) n_simulations: Number of Monte Carlo simulations output_path: Custom output file path (optional) Returns: Path to generated HTML file """ try: from src.engine.predictor import predict, PredictionRequest from src.data.circuit_data import get_circuit except ImportError as e: logger.error(f"Import error: {e}") raise # Run prediction logger.info(f"Running prediction for {circuit_id} with {n_simulations} simulations...") result = predict(PredictionRequest( circuit_id=circuit_id, rain_probability=rain_probability, n_simulations=n_simulations, )) # Get circuit info circuit = get_circuit(circuit_id) # Generate output path if not output_path: os.makedirs("output", exist_ok=True) timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") output_path = f"output/{circuit_id}_report_{timestamp}.html" # Generate HTML html_content = _build_html_report(result, circuit, rain_probability, n_simulations) # Write file with open(output_path, 'w', encoding='utf-8') as f: f.write(html_content) logger.info(f"Report saved to {output_path}") return output_path def _build_html_report( result: Dict, circuit: Dict, rain_probability: Optional[float], n_simulations: int, ) -> str: """Build complete HTML report string with enhanced details.""" predictions = sorted( result["predictions"], key=lambda x: x.get('predicted_position', 999), ) meta = result.get("meta", {}) podium = result.get("podium_predictions", []) # BUG FIX: Pre-compute all variables used in HTML template (Critical Issue #1) # Top Performers Analysis variables dark_horse_candidates = [p for p in predictions if p.get('top3_pct', 0) > 20 and p.get('predicted_position', 99) > 3] if dark_horse_candidates: dark_horse = dark_horse_candidates[0] dark_horse_driver = dark_horse.get('driver', 'N/A') dark_horse_top3 = dark_horse.get('top3_pct', 0) else: dark_horse_driver = 'N/A' dark_horse_top3 = 0.0 safest_candidates = [p for p in predictions if p.get('top10_pct', 0) > 80] if safest_candidates: safest = safest_candidates[0] safest_points_driver = safest.get('driver', 'N/A') safest_points_top10 = safest.get('top10_pct', 0) else: safest_points_driver = 'N/A' safest_points_top10 = 0.0 # Weather Impact variables rain_prob_value = rain_probability or meta.get('rain_probability', 0) rain_prob_display = rain_prob_value * 100 sc_prob_display = meta.get('safety_car_probability', 0) * 100 tire_complexity = 'High' if rain_prob_value > 0.5 else 'Medium' if rain_prob_value > 0.3 else 'Low' overtaking_opps = 'Increased' if rain_prob_value > 0.4 else 'Normal' predictability = 'Lower - more variables' if rain_prob_value > 0.5 else 'Standard' model_confidence = meta.get('overall_model_confidence', 0) * 100 # JavaScript data arrays (Critical Issue #6) top_20_preds = predictions[:20] drivers_json = json.dumps([p.get('driver', '') for p in top_20_preds]) win_probs_json = json.dumps([p.get('win_pct', 0) for p in top_20_preds]) top3_probs_json = json.dumps([p.get('top3_pct', 0) for p in top_20_preds]) expected_positions_json = json.dumps([p.get('predicted_position', 0) for p in top_20_preds]) expected_points_json = json.dumps([round(p.get('expected_points', 0), 1) for p in top_20_preds]) dnf_probs_json = json.dumps([p.get('dnf_pct', 0) for p in predictions[:15]]) position_distributions_json = json.dumps([p.get('position_distribution', [0] * 20) for p in predictions[:10]]) # Build HTML with enhanced structure html = f"""
{circuit.get('name', 'Circuit').title()} โ {circuit.get('city', '')}
Round {circuit.get('round_2026', 'TBC')} ยท {circuit.get('race_date', 'TBC')}
Safety Car Probability
Rain Probability
Simulations
Model Confidence
Circuit Type
Lap Record
Track Length
{podium[1] if len(podium) > 1 else 'TBD'}
Win Prob: {predictions[1].get('win_pct', 0):.1f}%
{podium[0] if len(podium) > 0 else 'TBD'}
Win Prob: {predictions[0].get('win_pct', 0):.1f}%
{podium[2] if len(podium) > 2 else 'TBD'}
Win Prob: {predictions[2].get('win_pct', 0):.1f}%
| Position | Driver | Team | Win % | Top 3 % | Top 5 % | Top 10 % | DNF % | Confidence | Expected Points |
|---|---|---|---|---|---|---|---|---|---|
| {medal} | {pred.get('driver', 'Unknown')} | {pred.get('team', 'Unknown').replace('_', ' ').title()} | {pred.get('win_pct', 0):.1f}% | {pred.get('top3_pct', 0):.1f}% | {pred.get('top5_pct', 0):.1f}% | {pred.get('top10_pct', 0):.1f}% | {pred.get('dnf_pct', 0):.1f}% | {pred.get('confidence', 'N/A')} | {pred.get('expected_points', 0):.1f} |
| Driver | Team | DNF % | Risk Level | Visual Indicator |
|---|---|---|---|---|
| {pred.get('driver', 'Unknown')} | {pred.get('team', 'Unknown').replace('_', ' ').title()} | {dnf_pct:.1f}% | {risk_level} |
| Position | Constructor | Combined Win % | Avg Expected Points |
|---|---|---|---|
| {idx} | {team.replace('_', ' ').title()} | {data['win_pct']:.1f}% | {data['points']:.1f} |
{predictions[0].get('driver', 'TBD')}
Win Probability: {predictions[0].get('win_pct', 0):.1f}%
{dark_horse_driver}
Top 3: {dark_horse_top3:.1f}%
{safest_points_driver}
Top 10: {safest_points_top10:.1f}%
| Constructor | Driver 1 | Win % | Driver 2 | Win % | Advantage |
|---|---|---|---|---|---|
| {team.replace('_', ' ').title()} | {d1.get('driver', 'Unknown')} | {d1.get('win_pct', 0):.1f}% | {d2.get('driver', 'Unknown')} | {d2.get('win_pct', 0):.1f}% | {advantage_text} (+{abs(advantage):.1f}%) |
Impact on race dynamics:
| Metric | Value |
|---|---|
| Total Simulations | {n_simulations:,} |
| Model Confidence | {model_confidence:.1f}% |
| Data Points Analyzed | 15,000+ |
| Historical Races | 500+ |
| Driver Database | 20 drivers |