f1_nexus_wasm/
lib.rs

1//! F1 Nexus WASM Module
2//!
3//! Browser-based strategy optimization, simulation, and visualization
4
5use wasm_bindgen::prelude::*;
6use f1_nexus_core::*;
7use f1_nexus_strategy::*;
8use f1_nexus_strategy::simulation::*;
9use serde::{Deserialize, Serialize};
10use std::collections::BTreeMap;
11
12#[wasm_bindgen]
13extern "C" {
14    #[wasm_bindgen(js_namespace = console)]
15    fn log(s: &str);
16
17    #[wasm_bindgen(js_namespace = console)]
18    fn error(s: &str);
19}
20
21/// Initialize the WASM module
22#[wasm_bindgen(start)]
23pub fn init() {
24    console_error_panic_hook::set_once();
25    log("F1 Nexus WASM module initialized v1.0.0-alpha.1");
26}
27
28/// F1 Nexus WASM API
29#[wasm_bindgen]
30pub struct F1Nexus;
31
32#[wasm_bindgen]
33impl F1Nexus {
34    #[wasm_bindgen(constructor)]
35    pub fn new() -> Self {
36        F1Nexus
37    }
38
39    /// Get version
40    #[wasm_bindgen]
41    pub fn version(&self) -> String {
42        env!("CARGO_PKG_VERSION").to_string()
43    }
44
45    /// Optimize pit stop strategy
46    ///
47    /// Input format (JSON):
48    /// ```json
49    /// {
50    ///   "track": "monaco",
51    ///   "total_laps": 78,
52    ///   "starting_fuel": 110.0,
53    ///   "position": 5,
54    ///   "available_compounds": ["C1", "C2", "C3"]
55    /// }
56    /// ```
57    #[wasm_bindgen]
58    pub fn optimize_strategy(&self, params: JsValue) -> Result<JsValue, JsValue> {
59        let input: OptimizeInput = serde_wasm_bindgen::from_value(params)
60            .map_err(|e| JsValue::from_str(&format!("Invalid input: {}", e)))?;
61
62        // Create circuit
63        let circuit = create_circuit(&input.track);
64
65        // Parse tire compounds
66        let available_compounds: Vec<TireCompound> = input
67            .available_compounds
68            .unwrap_or_else(|| vec!["C1".to_string(), "C2".to_string(), "C3".to_string()])
69            .iter()
70            .filter_map(|s| parse_tire_compound(s))
71            .collect();
72
73        if available_compounds.is_empty() {
74            return Err(JsValue::from_str("No valid tire compounds provided"));
75        }
76
77        // Setup optimization config
78        let config = OptimizationConfig {
79            total_laps: input.total_laps.unwrap_or(circuit.typical_race_laps),
80            circuit: circuit.clone(),
81            available_compounds,
82            pit_lane_time_loss: 20.0,
83            tire_change_time: 2.5,
84            current_position: input.position.unwrap_or(5) as u8,
85            competitors_ahead: vec![],
86            degradation_factors: DegradationFactors::default(),
87            fuel_model: FuelConsumptionModel::default_model(),
88            starting_fuel: input.starting_fuel.unwrap_or(110.0),
89            min_pit_stops: 1,
90            max_pit_stops: 3,
91        };
92
93        // Optimize strategy
94        let strategy = optimize_pit_strategy(&config)
95            .map_err(|e| JsValue::from_str(&format!("Optimization failed: {}", e)))?;
96
97        // Convert to JSON-friendly format
98        let output = OptimizeOutput {
99            strategy_id: strategy.id,
100            starting_compound: format!("{:?}", strategy.starting_compound),
101            pit_stops: strategy.pit_stops.iter().map(|stop| PitStopOutput {
102                lap: stop.lap.0,
103                compound: format!("{:?}", stop.compound),
104                pit_loss: stop.pit_loss,
105                reason: format!("{:?}", stop.reason),
106                confidence: stop.confidence,
107            }).collect(),
108            predicted_race_time: strategy.predicted_race_time,
109            confidence: strategy.confidence,
110        };
111
112        serde_wasm_bindgen::to_value(&output)
113            .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
114    }
115
116    /// Simulate race with given strategy
117    ///
118    /// Input format (JSON):
119    /// ```json
120    /// {
121    ///   "track": "spa",
122    ///   "num_simulations": 100,
123    ///   "pit_stops": [{"lap": 22, "compound": "C2"}]
124    /// }
125    /// ```
126    #[wasm_bindgen]
127    pub fn simulate_race(&self, params: JsValue) -> Result<JsValue, JsValue> {
128        let input: SimulateInput = serde_wasm_bindgen::from_value(params)
129            .map_err(|e| JsValue::from_str(&format!("Invalid input: {}", e)))?;
130
131        // Create circuit
132        let circuit = create_circuit(&input.track);
133
134        // Parse pit stops
135        let pit_stops: Vec<PitStop> = input.pit_stops.iter().map(|stop| {
136            let compound = parse_tire_compound(&stop.compound).unwrap_or(TireCompound::C3);
137            PitStop {
138                lap: LapNumber(stop.lap),
139                compound,
140                pit_loss: 22.0,
141                reason: PitStopReason::Mandatory,
142                confidence: 0.85,
143            }
144        }).collect();
145
146        // Create strategy
147        let strategy = RaceStrategy {
148            id: format!("wasm-sim-{}", chrono::Utc::now().timestamp()),
149            starting_compound: parse_tire_compound(&input.starting_compound.unwrap_or("C3".to_string()))
150                .unwrap_or(TireCompound::C3),
151            pit_stops,
152            fuel_strategy: FuelStrategy {
153                starting_fuel: 110.0,
154                fuel_saving_per_lap: 0.0,
155                fuel_saving_laps: vec![],
156                minimum_buffer: 3.0,
157            },
158            ers_plan: ErsDeploymentPlan {
159                default_mode: f1_nexus_core::strategy::ErsMode::Medium,
160                lap_overrides: BTreeMap::new(),
161                overtake_laps: vec![],
162            },
163            expected_lap_times: BTreeMap::new(),
164            predicted_race_time: 0.0,
165            confidence: 0.8,
166            metadata: StrategyMetadata {
167                generated_at: chrono::Utc::now(),
168                num_simulations: input.num_simulations.unwrap_or(100),
169                contributing_agents: vec!["wasm".to_string()],
170                version_hash: None,
171                parent_strategy_id: None,
172            },
173        };
174
175        // Create weather
176        let weather = WeatherConditions {
177            initial_condition: WeatherCondition::Dry,
178            track_temperature: 30.0,
179            air_temperature: 25.0,
180            changes: vec![],
181        };
182
183        // Create simulator
184        let simulator = RaceSimulator::new(
185            circuit,
186            strategy,
187            FuelConsumptionModel::default_model(),
188            weather,
189        );
190
191        // Run simulations
192        let num_sims = input.num_simulations.unwrap_or(100);
193        let mut total_time = 0.0_f32;
194        let mut min_time = f32::INFINITY;
195        let mut max_time = 0.0_f32;
196
197        for _ in 0..num_sims {
198            let result = simulator.simulate_race();
199            total_time += result.total_time;
200            min_time = min_time.min(result.total_time);
201            max_time = max_time.max(result.total_time);
202        }
203
204        let mean_time = total_time / num_sims as f32;
205
206        // Run one detailed simulation
207        let sample = simulator.simulate_race();
208
209        let output = SimulateOutput {
210            num_simulations: num_sims,
211            mean_race_time: mean_time,
212            min_race_time: min_time,
213            max_race_time: max_time,
214            total_laps: sample.lap_times.len() as u16,
215            pit_stops: sample.pit_stops.len() as u8,
216            final_fuel_kg: sample.fuel_history.last().copied().unwrap_or(0.0),
217            fastest_lap: sample.lap_times.iter().fold(f32::INFINITY, |a, &b| a.min(b)),
218        };
219
220        serde_wasm_bindgen::to_value(&output)
221            .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
222    }
223
224    /// Predict tire life
225    ///
226    /// Input format (JSON):
227    /// ```json
228    /// {
229    ///   "compound": "C3",
230    ///   "age_laps": 15,
231    ///   "track_temp": 32.0,
232    ///   "track_severity": 1.2
233    /// }
234    /// ```
235    #[wasm_bindgen]
236    pub fn predict_tire_life(&self, params: JsValue) -> Result<JsValue, JsValue> {
237        let input: TireLifeInput = serde_wasm_bindgen::from_value(params)
238            .map_err(|e| JsValue::from_str(&format!("Invalid input: {}", e)))?;
239
240        let compound = parse_tire_compound(&input.compound)
241            .ok_or_else(|| JsValue::from_str("Invalid tire compound"))?;
242
243        let tire_chars = f1_nexus_core::tire::TireCharacteristics::for_compound(compound);
244
245        let track_severity = input.track_severity.unwrap_or(1.0);
246        let current_wear = input.age_laps as f32 * tire_chars.degradation_rate * track_severity;
247        let current_wear = current_wear.min(1.0);
248
249        let grip_multiplier = tire_chars.grip_multiplier_for_temp(input.track_temp.unwrap_or(100.0));
250        let remaining_laps = tire_chars.predict_remaining_life(current_wear, track_severity);
251
252        let output = TireLifeOutput {
253            compound: input.compound,
254            current_age_laps: input.age_laps,
255            current_wear_percent: current_wear * 100.0,
256            typical_life_laps: tire_chars.typical_life,
257            estimated_remaining_laps: remaining_laps.min(tire_chars.typical_life as f32),
258            grip_multiplier,
259            recommended_pit_soon: current_wear > 0.7 || remaining_laps < 5.0,
260        };
261
262        serde_wasm_bindgen::to_value(&output)
263            .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
264    }
265
266    /// Get list of supported circuits
267    #[wasm_bindgen]
268    pub fn get_circuits(&self) -> JsValue {
269        let circuits = vec![
270            "monaco", "spa", "silverstone", "monza", "suzuka",
271            "interlagos", "austin", "barcelona", "austria", "hungary",
272        ];
273        serde_wasm_bindgen::to_value(&circuits).unwrap()
274    }
275
276    /// Get list of tire compounds
277    #[wasm_bindgen]
278    pub fn get_tire_compounds(&self) -> JsValue {
279        let compounds = vec!["C0", "C1", "C2", "C3", "C4", "C5", "Intermediate", "Wet"];
280        serde_wasm_bindgen::to_value(&compounds).unwrap()
281    }
282}
283
284impl Default for F1Nexus {
285    fn default() -> Self {
286        Self::new()
287    }
288}
289
290// Input/Output types
291
292#[derive(Deserialize)]
293struct OptimizeInput {
294    track: String,
295    total_laps: Option<u16>,
296    starting_fuel: Option<f32>,
297    position: Option<u16>,
298    available_compounds: Option<Vec<String>>,
299}
300
301#[derive(Serialize)]
302struct OptimizeOutput {
303    strategy_id: String,
304    starting_compound: String,
305    pit_stops: Vec<PitStopOutput>,
306    predicted_race_time: f32,
307    confidence: f32,
308}
309
310#[derive(Serialize)]
311struct PitStopOutput {
312    lap: u16,
313    compound: String,
314    pit_loss: f32,
315    reason: String,
316    confidence: f32,
317}
318
319#[derive(Deserialize)]
320struct SimulateInput {
321    track: String,
322    num_simulations: Option<u64>,
323    starting_compound: Option<String>,
324    pit_stops: Vec<PitStopInput>,
325}
326
327#[derive(Deserialize)]
328struct PitStopInput {
329    lap: u16,
330    compound: String,
331}
332
333#[derive(Serialize)]
334struct SimulateOutput {
335    num_simulations: u64,
336    mean_race_time: f32,
337    min_race_time: f32,
338    max_race_time: f32,
339    total_laps: u16,
340    pit_stops: u8,
341    final_fuel_kg: f32,
342    fastest_lap: f32,
343}
344
345#[derive(Deserialize)]
346struct TireLifeInput {
347    compound: String,
348    age_laps: u16,
349    track_temp: Option<f32>,
350    track_severity: Option<f32>,
351}
352
353#[derive(Serialize)]
354struct TireLifeOutput {
355    compound: String,
356    current_age_laps: u16,
357    current_wear_percent: f32,
358    typical_life_laps: u16,
359    estimated_remaining_laps: f32,
360    grip_multiplier: f32,
361    recommended_pit_soon: bool,
362}
363
364// Helper functions
365
366fn parse_tire_compound(s: &str) -> Option<TireCompound> {
367    match s.to_uppercase().as_str() {
368        "C0" => Some(TireCompound::C0),
369        "C1" => Some(TireCompound::C1),
370        "C2" => Some(TireCompound::C2),
371        "C3" => Some(TireCompound::C3),
372        "C4" => Some(TireCompound::C4),
373        "C5" => Some(TireCompound::C5),
374        "INTERMEDIATE" | "INT" => Some(TireCompound::Intermediate),
375        "WET" => Some(TireCompound::Wet),
376        _ => None,
377    }
378}
379
380fn create_circuit(track_id: &str) -> Circuit {
381    match track_id.to_lowercase().as_str() {
382        "monaco" => Circuit {
383            id: "monaco".to_string(),
384            name: "Circuit de Monaco".to_string(),
385            country: "Monaco".to_string(),
386            length: 3337.0,
387            num_turns: 19,
388            lap_record: 70.0,
389            characteristics: TrackCharacteristics {
390                tire_severity: 1.2,
391                fuel_consumption: 0.9,
392                overtaking_difficulty: 0.95,
393                downforce_level: 0.9,
394                average_speed: 160.0,
395                maximum_speed: 290.0,
396                elevation_change: 42.0,
397                weather_variability: 0.3,
398            },
399            sectors: vec![],
400            drs_zones: vec![],
401            typical_race_laps: 78,
402        },
403        "spa" | "spa-francorchamps" => Circuit {
404            id: "spa".to_string(),
405            name: "Circuit de Spa-Francorchamps".to_string(),
406            country: "Belgium".to_string(),
407            length: 7004.0,
408            num_turns: 19,
409            lap_record: 103.0,
410            characteristics: TrackCharacteristics {
411                tire_severity: 0.85,
412                fuel_consumption: 1.3,
413                overtaking_difficulty: 0.6,
414                downforce_level: 0.65,
415                average_speed: 230.0,
416                maximum_speed: 340.0,
417                elevation_change: 105.0,
418                weather_variability: 0.8,
419            },
420            sectors: vec![],
421            drs_zones: vec![],
422            typical_race_laps: 44,
423        },
424        "silverstone" => Circuit {
425            id: "silverstone".to_string(),
426            name: "Silverstone Circuit".to_string(),
427            country: "United Kingdom".to_string(),
428            length: 5891.0,
429            num_turns: 18,
430            lap_record: 85.0,
431            characteristics: TrackCharacteristics {
432                tire_severity: 1.1,
433                fuel_consumption: 1.1,
434                overtaking_difficulty: 0.65,
435                downforce_level: 0.75,
436                average_speed: 240.0,
437                maximum_speed: 320.0,
438                elevation_change: 18.0,
439                weather_variability: 0.7,
440            },
441            sectors: vec![],
442            drs_zones: vec![],
443            typical_race_laps: 52,
444        },
445        _ => Circuit {
446            id: track_id.to_string(),
447            name: format!("Circuit {}", track_id),
448            country: "Unknown".to_string(),
449            length: 5000.0,
450            num_turns: 16,
451            lap_record: 90.0,
452            characteristics: TrackCharacteristics {
453                tire_severity: 1.0,
454                fuel_consumption: 1.0,
455                overtaking_difficulty: 0.7,
456                downforce_level: 0.7,
457                average_speed: 210.0,
458                maximum_speed: 310.0,
459                elevation_change: 20.0,
460                weather_variability: 0.5,
461            },
462            sectors: vec![],
463            drs_zones: vec![],
464            typical_race_laps: 60,
465        },
466    }
467}
468
469#[cfg(test)]
470mod tests {
471    use super::*;
472    use wasm_bindgen_test::*;
473
474    #[wasm_bindgen_test]
475    fn test_version() {
476        let nexus = F1Nexus::new();
477        let version = nexus.version();
478        assert!(!version.is_empty());
479    }
480
481    #[test]
482    fn test_parse_tire_compound() {
483        assert_eq!(parse_tire_compound("C3"), Some(TireCompound::C3));
484        assert_eq!(parse_tire_compound("c3"), Some(TireCompound::C3));
485        assert_eq!(parse_tire_compound("intermediate"), Some(TireCompound::Intermediate));
486        assert_eq!(parse_tire_compound("invalid"), None);
487    }
488
489    #[test]
490    fn test_create_circuit() {
491        let monaco = create_circuit("monaco");
492        assert_eq!(monaco.id, "monaco");
493        assert_eq!(monaco.typical_race_laps, 78);
494
495        let spa = create_circuit("spa");
496        assert_eq!(spa.id, "spa");
497        assert_eq!(spa.typical_race_laps, 44);
498    }
499}