1#![deny(clippy::all)]
6
7use napi::bindgen_prelude::*;
8use napi_derive::napi;
9use f1_nexus_core::*;
10use f1_nexus_strategy::*;
11use f1_nexus_strategy::simulation::*;
12use serde::{Deserialize, Serialize};
13use std::collections::BTreeMap;
14
15#[napi]
17pub fn version() -> String {
18 env!("CARGO_PKG_VERSION").to_string()
19}
20
21#[napi]
23pub fn get_circuits() -> Vec<String> {
24 vec![
25 "monaco".to_string(),
26 "spa".to_string(),
27 "silverstone".to_string(),
28 "monza".to_string(),
29 "suzuka".to_string(),
30 "interlagos".to_string(),
31 "austin".to_string(),
32 "barcelona".to_string(),
33 "austria".to_string(),
34 "hungary".to_string(),
35 ]
36}
37
38#[napi]
40pub fn get_tire_compounds() -> Vec<String> {
41 vec![
42 "C0".to_string(),
43 "C1".to_string(),
44 "C2".to_string(),
45 "C3".to_string(),
46 "C4".to_string(),
47 "C5".to_string(),
48 "Intermediate".to_string(),
49 "Wet".to_string(),
50 ]
51}
52
53#[napi]
69pub fn optimize_strategy(params_json: String) -> Result<String> {
70 let input: OptimizeInput = serde_json::from_str(¶ms_json)
71 .map_err(|e| Error::from_reason(format!("Invalid input JSON: {}", e)))?;
72
73 let circuit = create_circuit(&input.track);
75
76 let available_compounds: Vec<TireCompound> = input
78 .available_compounds
79 .unwrap_or_else(|| vec!["C1".to_string(), "C2".to_string(), "C3".to_string()])
80 .iter()
81 .filter_map(|s| parse_tire_compound(s))
82 .collect();
83
84 if available_compounds.is_empty() {
85 return Err(Error::from_reason("No valid tire compounds provided"));
86 }
87
88 let config = OptimizationConfig {
90 total_laps: input.total_laps.unwrap_or(circuit.typical_race_laps),
91 circuit: circuit.clone(),
92 available_compounds,
93 pit_lane_time_loss: 20.0,
94 tire_change_time: 2.5,
95 current_position: input.position.unwrap_or(5) as u8,
96 competitors_ahead: vec![],
97 degradation_factors: DegradationFactors::default(),
98 fuel_model: FuelConsumptionModel::default_model(),
99 starting_fuel: input.starting_fuel.unwrap_or(110.0),
100 min_pit_stops: 1,
101 max_pit_stops: 3,
102 };
103
104 let strategy = optimize_pit_strategy(&config)
106 .map_err(|e| Error::from_reason(format!("Optimization failed: {}", e)))?;
107
108 let output = OptimizeOutput {
110 strategy_id: strategy.id,
111 starting_compound: format!("{:?}", strategy.starting_compound),
112 pit_stops: strategy.pit_stops.iter().map(|stop| PitStopOutput {
113 lap: stop.lap.0,
114 compound: format!("{:?}", stop.compound),
115 pit_loss: stop.pit_loss,
116 reason: format!("{:?}", stop.reason),
117 confidence: stop.confidence,
118 }).collect(),
119 predicted_race_time: strategy.predicted_race_time,
120 confidence: strategy.confidence,
121 };
122
123 serde_json::to_string(&output)
124 .map_err(|e| Error::from_reason(format!("Serialization error: {}", e)))
125}
126
127#[napi]
142pub fn simulate_race(params_json: String) -> Result<String> {
143 let input: SimulateInput = serde_json::from_str(¶ms_json)
144 .map_err(|e| Error::from_reason(format!("Invalid input JSON: {}", e)))?;
145
146 let circuit = create_circuit(&input.track);
148
149 let pit_stops: Vec<PitStop> = input.pit_stops.iter().map(|stop| {
151 let compound = parse_tire_compound(&stop.compound).unwrap_or(TireCompound::C3);
152 PitStop {
153 lap: LapNumber(stop.lap),
154 compound,
155 pit_loss: 22.0,
156 reason: PitStopReason::Mandatory,
157 confidence: 0.85,
158 }
159 }).collect();
160
161 let strategy = RaceStrategy {
163 id: format!("node-sim-{}", chrono::Utc::now().timestamp()),
164 starting_compound: parse_tire_compound(&input.starting_compound.unwrap_or("C3".to_string()))
165 .unwrap_or(TireCompound::C3),
166 pit_stops,
167 fuel_strategy: FuelStrategy {
168 starting_fuel: 110.0,
169 fuel_saving_per_lap: 0.0,
170 fuel_saving_laps: vec![],
171 minimum_buffer: 3.0,
172 },
173 ers_plan: ErsDeploymentPlan {
174 default_mode: f1_nexus_core::strategy::ErsMode::Medium,
175 lap_overrides: BTreeMap::new(),
176 overtake_laps: vec![],
177 },
178 expected_lap_times: BTreeMap::new(),
179 predicted_race_time: 0.0,
180 confidence: 0.8,
181 metadata: StrategyMetadata {
182 generated_at: chrono::Utc::now(),
183 num_simulations: input.num_simulations.unwrap_or(100),
184 contributing_agents: vec!["node".to_string()],
185 version_hash: None,
186 parent_strategy_id: None,
187 },
188 };
189
190 let weather = WeatherConditions {
192 initial_condition: WeatherCondition::Dry,
193 track_temperature: 30.0,
194 air_temperature: 25.0,
195 changes: vec![],
196 };
197
198 let simulator = RaceSimulator::new(
200 circuit,
201 strategy,
202 FuelConsumptionModel::default_model(),
203 weather,
204 );
205
206 let num_sims = input.num_simulations.unwrap_or(100);
208 let mut total_time = 0.0_f32;
209 let mut min_time = f32::INFINITY;
210 let mut max_time = 0.0_f32;
211
212 for _ in 0..num_sims {
213 let result = simulator.simulate_race();
214 total_time += result.total_time;
215 min_time = min_time.min(result.total_time);
216 max_time = max_time.max(result.total_time);
217 }
218
219 let mean_time = total_time / num_sims as f32;
220
221 let sample = simulator.simulate_race();
223
224 let output = SimulateOutput {
225 num_simulations: num_sims,
226 mean_race_time: mean_time,
227 min_race_time: min_time,
228 max_race_time: max_time,
229 total_laps: sample.lap_times.len() as u16,
230 pit_stops: sample.pit_stops.len() as u8,
231 final_fuel_kg: sample.fuel_history.last().copied().unwrap_or(0.0),
232 fastest_lap: sample.lap_times.iter().fold(f32::INFINITY, |a, &b| a.min(b)),
233 };
234
235 serde_json::to_string(&output)
236 .map_err(|e| Error::from_reason(format!("Serialization error: {}", e)))
237}
238
239#[napi]
254pub fn predict_tire_life(params_json: String) -> Result<String> {
255 let input: TireLifeInput = serde_json::from_str(¶ms_json)
256 .map_err(|e| Error::from_reason(format!("Invalid input JSON: {}", e)))?;
257
258 let compound = parse_tire_compound(&input.compound)
259 .ok_or_else(|| Error::from_reason("Invalid tire compound"))?;
260
261 let tire_chars = f1_nexus_core::tire::TireCharacteristics::for_compound(compound);
262
263 let track_severity = input.track_severity.unwrap_or(1.0);
264 let current_wear = input.age_laps as f32 * tire_chars.degradation_rate * track_severity;
265 let current_wear = current_wear.min(1.0);
266
267 let grip_multiplier = tire_chars.grip_multiplier_for_temp(input.track_temp.unwrap_or(100.0));
268 let remaining_laps = tire_chars.predict_remaining_life(current_wear, track_severity);
269
270 let output = TireLifeOutput {
271 compound: input.compound,
272 current_age_laps: input.age_laps,
273 current_wear_percent: current_wear * 100.0,
274 typical_life_laps: tire_chars.typical_life,
275 estimated_remaining_laps: remaining_laps.min(tire_chars.typical_life as f32),
276 grip_multiplier,
277 recommended_pit_soon: current_wear > 0.7 || remaining_laps < 5.0,
278 };
279
280 serde_json::to_string(&output)
281 .map_err(|e| Error::from_reason(format!("Serialization error: {}", e)))
282}
283
284#[derive(Deserialize)]
287struct OptimizeInput {
288 track: String,
289 total_laps: Option<u16>,
290 starting_fuel: Option<f32>,
291 position: Option<u16>,
292 available_compounds: Option<Vec<String>>,
293}
294
295#[derive(Serialize)]
296struct OptimizeOutput {
297 strategy_id: String,
298 starting_compound: String,
299 pit_stops: Vec<PitStopOutput>,
300 predicted_race_time: f32,
301 confidence: f32,
302}
303
304#[derive(Serialize)]
305struct PitStopOutput {
306 lap: u16,
307 compound: String,
308 pit_loss: f32,
309 reason: String,
310 confidence: f32,
311}
312
313#[derive(Deserialize)]
314struct SimulateInput {
315 track: String,
316 num_simulations: Option<u64>,
317 starting_compound: Option<String>,
318 pit_stops: Vec<PitStopInput>,
319}
320
321#[derive(Deserialize)]
322struct PitStopInput {
323 lap: u16,
324 compound: String,
325}
326
327#[derive(Serialize)]
328struct SimulateOutput {
329 num_simulations: u64,
330 mean_race_time: f32,
331 min_race_time: f32,
332 max_race_time: f32,
333 total_laps: u16,
334 pit_stops: u8,
335 final_fuel_kg: f32,
336 fastest_lap: f32,
337}
338
339#[derive(Deserialize)]
340struct TireLifeInput {
341 compound: String,
342 age_laps: u16,
343 track_temp: Option<f32>,
344 track_severity: Option<f32>,
345}
346
347#[derive(Serialize)]
348struct TireLifeOutput {
349 compound: String,
350 current_age_laps: u16,
351 current_wear_percent: f32,
352 typical_life_laps: u16,
353 estimated_remaining_laps: f32,
354 grip_multiplier: f32,
355 recommended_pit_soon: bool,
356}
357
358fn parse_tire_compound(s: &str) -> Option<TireCompound> {
361 match s.to_uppercase().as_str() {
362 "C0" => Some(TireCompound::C0),
363 "C1" => Some(TireCompound::C1),
364 "C2" => Some(TireCompound::C2),
365 "C3" => Some(TireCompound::C3),
366 "C4" => Some(TireCompound::C4),
367 "C5" => Some(TireCompound::C5),
368 "INTERMEDIATE" | "INT" => Some(TireCompound::Intermediate),
369 "WET" => Some(TireCompound::Wet),
370 _ => None,
371 }
372}
373
374fn create_circuit(track_id: &str) -> Circuit {
375 match track_id.to_lowercase().as_str() {
376 "monaco" => Circuit {
377 id: "monaco".to_string(),
378 name: "Circuit de Monaco".to_string(),
379 country: "Monaco".to_string(),
380 length: 3337.0,
381 num_turns: 19,
382 lap_record: 70.0,
383 characteristics: TrackCharacteristics {
384 tire_severity: 1.2,
385 fuel_consumption: 0.9,
386 overtaking_difficulty: 0.95,
387 downforce_level: 0.9,
388 average_speed: 160.0,
389 maximum_speed: 290.0,
390 elevation_change: 42.0,
391 weather_variability: 0.3,
392 },
393 sectors: vec![],
394 drs_zones: vec![],
395 typical_race_laps: 78,
396 },
397 "spa" | "spa-francorchamps" => Circuit {
398 id: "spa".to_string(),
399 name: "Circuit de Spa-Francorchamps".to_string(),
400 country: "Belgium".to_string(),
401 length: 7004.0,
402 num_turns: 19,
403 lap_record: 103.0,
404 characteristics: TrackCharacteristics {
405 tire_severity: 0.85,
406 fuel_consumption: 1.3,
407 overtaking_difficulty: 0.6,
408 downforce_level: 0.65,
409 average_speed: 230.0,
410 maximum_speed: 340.0,
411 elevation_change: 105.0,
412 weather_variability: 0.8,
413 },
414 sectors: vec![],
415 drs_zones: vec![],
416 typical_race_laps: 44,
417 },
418 "silverstone" => Circuit {
419 id: "silverstone".to_string(),
420 name: "Silverstone Circuit".to_string(),
421 country: "United Kingdom".to_string(),
422 length: 5891.0,
423 num_turns: 18,
424 lap_record: 85.0,
425 characteristics: TrackCharacteristics {
426 tire_severity: 1.1,
427 fuel_consumption: 1.1,
428 overtaking_difficulty: 0.65,
429 downforce_level: 0.75,
430 average_speed: 240.0,
431 maximum_speed: 320.0,
432 elevation_change: 18.0,
433 weather_variability: 0.7,
434 },
435 sectors: vec![],
436 drs_zones: vec![],
437 typical_race_laps: 52,
438 },
439 _ => Circuit {
440 id: track_id.to_string(),
441 name: format!("Circuit {}", track_id),
442 country: "Unknown".to_string(),
443 length: 5000.0,
444 num_turns: 16,
445 lap_record: 90.0,
446 characteristics: TrackCharacteristics {
447 tire_severity: 1.0,
448 fuel_consumption: 1.0,
449 overtaking_difficulty: 0.7,
450 downforce_level: 0.7,
451 average_speed: 210.0,
452 maximum_speed: 310.0,
453 elevation_change: 20.0,
454 weather_variability: 0.5,
455 },
456 sectors: vec![],
457 drs_zones: vec![],
458 typical_race_laps: 60,
459 },
460 }
461}