f1_nexus_node/
lib.rs

1//! F1 Nexus NAPI-RS bindings for Node.js
2//!
3//! High-performance native bindings for Node.js integration
4
5#![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/// F1 Nexus version
16#[napi]
17pub fn version() -> String {
18    env!("CARGO_PKG_VERSION").to_string()
19}
20
21/// Get list of supported circuits
22#[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/// Get list of tire compounds
39#[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/// Optimize pit stop strategy
54///
55/// # Arguments
56/// * `params_json` - JSON string with optimization parameters
57///
58/// # Example
59/// ```javascript
60/// const result = optimizeStrategy(JSON.stringify({
61///   track: "monaco",
62///   total_laps: 78,
63///   starting_fuel: 110.0,
64///   position: 5,
65///   available_compounds: ["C1", "C2", "C3"]
66/// }));
67/// ```
68#[napi]
69pub fn optimize_strategy(params_json: String) -> Result<String> {
70    let input: OptimizeInput = serde_json::from_str(&params_json)
71        .map_err(|e| Error::from_reason(format!("Invalid input JSON: {}", e)))?;
72
73    // Create circuit
74    let circuit = create_circuit(&input.track);
75
76    // Parse tire compounds
77    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    // Setup optimization config
89    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    // Optimize strategy
105    let strategy = optimize_pit_strategy(&config)
106        .map_err(|e| Error::from_reason(format!("Optimization failed: {}", e)))?;
107
108    // Convert to JSON-friendly format
109    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/// Simulate race with given strategy
128///
129/// # Arguments
130/// * `params_json` - JSON string with simulation parameters
131///
132/// # Example
133/// ```javascript
134/// const result = simulateRace(JSON.stringify({
135///   track: "spa",
136///   num_simulations: 100,
137///   starting_compound: "C3",
138///   pit_stops: [{lap: 22, compound: "C2"}]
139/// }));
140/// ```
141#[napi]
142pub fn simulate_race(params_json: String) -> Result<String> {
143    let input: SimulateInput = serde_json::from_str(&params_json)
144        .map_err(|e| Error::from_reason(format!("Invalid input JSON: {}", e)))?;
145
146    // Create circuit
147    let circuit = create_circuit(&input.track);
148
149    // Parse pit stops
150    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    // Create strategy
162    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    // Create weather
191    let weather = WeatherConditions {
192        initial_condition: WeatherCondition::Dry,
193        track_temperature: 30.0,
194        air_temperature: 25.0,
195        changes: vec![],
196    };
197
198    // Create simulator
199    let simulator = RaceSimulator::new(
200        circuit,
201        strategy,
202        FuelConsumptionModel::default_model(),
203        weather,
204    );
205
206    // Run simulations
207    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    // Run one detailed simulation
222    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/// Predict tire life
240///
241/// # Arguments
242/// * `params_json` - JSON string with tire life parameters
243///
244/// # Example
245/// ```javascript
246/// const result = predictTireLife(JSON.stringify({
247///   compound: "C3",
248///   age_laps: 15,
249///   track_temp: 32.0,
250///   track_severity: 1.2
251/// }));
252/// ```
253#[napi]
254pub fn predict_tire_life(params_json: String) -> Result<String> {
255    let input: TireLifeInput = serde_json::from_str(&params_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// Input/Output types
285
286#[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
358// Helper functions
359
360fn 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}