1use 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#[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#[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 #[wasm_bindgen]
41 pub fn version(&self) -> String {
42 env!("CARGO_PKG_VERSION").to_string()
43 }
44
45 #[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 let circuit = create_circuit(&input.track);
64
65 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 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 let strategy = optimize_pit_strategy(&config)
95 .map_err(|e| JsValue::from_str(&format!("Optimization failed: {}", e)))?;
96
97 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 #[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 let circuit = create_circuit(&input.track);
133
134 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 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 let weather = WeatherConditions {
177 initial_condition: WeatherCondition::Dry,
178 track_temperature: 30.0,
179 air_temperature: 25.0,
180 changes: vec![],
181 };
182
183 let simulator = RaceSimulator::new(
185 circuit,
186 strategy,
187 FuelConsumptionModel::default_model(),
188 weather,
189 );
190
191 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 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 #[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 #[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 #[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#[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
364fn 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}