ballistics_engine/
cli_api.rs

1// CLI API module - provides simplified interfaces for command-line tool
2use crate::DragModel;
3use nalgebra::Vector3;
4use std::error::Error;
5use std::fmt;
6
7// Error type for CLI operations
8#[derive(Debug)]
9pub struct BallisticsError {
10    message: String,
11}
12
13impl fmt::Display for BallisticsError {
14    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
15        write!(f, "{}", self.message)
16    }
17}
18
19impl Error for BallisticsError {}
20
21impl From<String> for BallisticsError {
22    fn from(msg: String) -> Self {
23        BallisticsError { message: msg }
24    }
25}
26
27impl From<&str> for BallisticsError {
28    fn from(msg: &str) -> Self {
29        BallisticsError { message: msg.to_string() }
30    }
31}
32
33// Ballistic input parameters
34#[derive(Debug, Clone)]
35pub struct BallisticInputs {
36    // Core ballistics parameters
37    pub muzzle_velocity: f64,      // m/s
38    pub launch_angle: f64,          // radians (same as muzzle_angle)
39    pub ballistic_coefficient: f64,
40    pub mass: f64,                  // kg
41    pub diameter: f64,              // meters
42    pub drag_model: DragModel,
43    pub sight_height: f64,          // meters
44    
45    // Additional fields for compatibility
46    pub altitude: f64,              // meters
47    pub bc_type: DragModel,         // same as drag_model
48    pub bc_value: f64,              // same as ballistic_coefficient
49    pub caliber_inches: f64,        // diameter in inches
50    pub weight_grains: f64,         // mass in grains
51    pub bullet_diameter: f64,       // same as diameter
52    pub bullet_mass: f64,           // same as mass
53    pub bullet_length: f64,         // meters
54    pub muzzle_angle: f64,          // same as launch_angle
55    pub target_distance: f64,       // meters
56    pub temperature: f64,           // Celsius
57    pub twist_rate: f64,            // inches per turn
58    pub is_twist_right: bool,       // right-hand twist
59    pub shooting_angle: f64,        // uphill/downhill angle in radians
60    pub latitude: Option<f64>,      // degrees
61    pub ground_threshold: f64,      // meters below which to stop
62    
63    // Advanced effects flags
64    pub enable_advanced_effects: bool,
65    pub use_powder_sensitivity: bool,
66    pub powder_temp_sensitivity: f64,
67    pub powder_temp: f64,          // Celsius
68    pub tipoff_yaw: f64,            // radians
69    pub tipoff_decay_distance: f64, // meters
70    pub use_bc_segments: bool,
71    pub bc_segments: Option<Vec<(f64, f64)>>,  // Mach-BC pairs
72    pub bc_segments_data: Option<Vec<crate::BCSegmentData>>,  // Velocity-BC segments
73    pub use_enhanced_spin_drift: bool,
74    pub use_form_factor: bool,
75    pub enable_wind_shear: bool,
76    pub wind_shear_model: String,
77    
78    // Additional data fields
79    pub bc_type_str: Option<String>,
80    pub bullet_model: Option<String>,
81    pub bullet_id: Option<String>,
82}
83
84impl Default for BallisticInputs {
85    fn default() -> Self {
86        let mass_kg = 0.01;
87        let diameter_m = 0.00762;
88        let bc = 0.5;
89        let launch_angle_rad = 0.0;
90        let drag_model = DragModel::G1;
91        
92        Self {
93            // Core parameters
94            muzzle_velocity: 800.0,
95            launch_angle: launch_angle_rad,
96            ballistic_coefficient: bc,
97            mass: mass_kg,
98            diameter: diameter_m,
99            drag_model,
100            sight_height: 0.05,
101            
102            // Duplicate fields for compatibility
103            altitude: 0.0,
104            bc_type: drag_model,
105            bc_value: bc,
106            caliber_inches: diameter_m / 0.0254,  // Convert to inches
107            weight_grains: mass_kg / 0.00006479891,  // Convert to grains
108            bullet_diameter: diameter_m,
109            bullet_mass: mass_kg,
110            bullet_length: diameter_m * 4.0,  // Approximate
111            muzzle_angle: launch_angle_rad,
112            target_distance: 100.0,
113            temperature: 15.0,
114            twist_rate: 12.0,  // 1:12" typical
115            is_twist_right: true,
116            shooting_angle: 0.0,
117            latitude: None,
118            ground_threshold: -10.0,
119            
120            // Advanced effects (disabled by default)
121            enable_advanced_effects: false,
122            use_powder_sensitivity: false,
123            powder_temp_sensitivity: 0.0,
124            powder_temp: 15.0,
125            tipoff_yaw: 0.0,
126            tipoff_decay_distance: 50.0,
127            use_bc_segments: false,
128            bc_segments: None,
129            bc_segments_data: None,
130            use_enhanced_spin_drift: false,
131            use_form_factor: false,
132            enable_wind_shear: false,
133            wind_shear_model: "none".to_string(),
134            
135            // Optional data
136            bc_type_str: None,
137            bullet_model: None,
138            bullet_id: None,
139        }
140    }
141}
142
143// Wind conditions
144#[derive(Debug, Clone)]
145pub struct WindConditions {
146    pub speed: f64,        // m/s
147    pub direction: f64,    // radians (0 = North, PI/2 = East)
148}
149
150impl Default for WindConditions {
151    fn default() -> Self {
152        Self {
153            speed: 0.0,
154            direction: 0.0,
155        }
156    }
157}
158
159// Atmospheric conditions
160#[derive(Debug, Clone)]
161pub struct AtmosphericConditions {
162    pub temperature: f64,  // Celsius
163    pub pressure: f64,     // hPa
164    pub humidity: f64,     // percentage (0-100)
165    pub altitude: f64,     // meters
166}
167
168impl Default for AtmosphericConditions {
169    fn default() -> Self {
170        Self {
171            temperature: 15.0,
172            pressure: 1013.25,
173            humidity: 50.0,
174            altitude: 0.0,
175        }
176    }
177}
178
179// Trajectory point data
180#[derive(Debug, Clone)]
181pub struct TrajectoryPoint {
182    pub time: f64,
183    pub position: Vector3<f64>,
184    pub velocity_magnitude: f64,
185    pub kinetic_energy: f64,
186}
187
188// Trajectory result
189#[derive(Debug, Clone)]
190pub struct TrajectoryResult {
191    pub max_range: f64,
192    pub max_height: f64,
193    pub time_of_flight: f64,
194    pub impact_velocity: f64,
195    pub impact_energy: f64,
196    pub points: Vec<TrajectoryPoint>,
197}
198
199// Trajectory solver
200pub struct TrajectorySolver {
201    inputs: BallisticInputs,
202    wind: WindConditions,
203    atmosphere: AtmosphericConditions,
204    max_range: f64,
205    time_step: f64,
206}
207
208impl TrajectorySolver {
209    pub fn new(mut inputs: BallisticInputs, wind: WindConditions, atmosphere: AtmosphericConditions) -> Self {
210        // Ensure duplicate fields are synchronized
211        inputs.bc_type = inputs.drag_model;
212        inputs.bc_value = inputs.ballistic_coefficient;
213        inputs.bullet_diameter = inputs.diameter;
214        inputs.bullet_mass = inputs.mass;
215        inputs.muzzle_angle = inputs.launch_angle;
216        inputs.caliber_inches = inputs.diameter / 0.0254;
217        inputs.weight_grains = inputs.mass / 0.00006479891;
218        
219        Self {
220            inputs,
221            wind,
222            atmosphere,
223            max_range: 1000.0,
224            time_step: 0.001,
225        }
226    }
227    
228    pub fn set_max_range(&mut self, range: f64) {
229        self.max_range = range;
230    }
231    
232    pub fn set_time_step(&mut self, step: f64) {
233        self.time_step = step;
234    }
235    
236    pub fn solve(&self) -> Result<TrajectoryResult, BallisticsError> {
237        // Simple trajectory integration using Euler method
238        let mut time = 0.0;
239        let mut position = Vector3::new(0.0, self.inputs.sight_height, 0.0);
240        let mut velocity = Vector3::new(
241            self.inputs.muzzle_velocity * self.inputs.launch_angle.cos(),
242            self.inputs.muzzle_velocity * self.inputs.launch_angle.sin(),
243            0.0,
244        );
245        
246        let mut points = Vec::new();
247        let mut max_height = position.y;
248        
249        // Calculate air density
250        let air_density = calculate_air_density(&self.atmosphere);
251        
252        // Wind vector
253        let wind_vector = Vector3::new(
254            self.wind.speed * self.wind.direction.sin(),
255            0.0,
256            self.wind.speed * self.wind.direction.cos(),
257        );
258        
259        // Main integration loop
260        while position.x < self.max_range && position.y >= 0.0 && time < 100.0 {
261            // Store trajectory point
262            let velocity_magnitude = velocity.magnitude();
263            let kinetic_energy = 0.5 * self.inputs.mass * velocity_magnitude * velocity_magnitude;
264            
265            points.push(TrajectoryPoint {
266                time,
267                position: position.clone(),
268                velocity_magnitude,
269                kinetic_energy,
270            });
271            
272            // Track max height
273            if position.y > max_height {
274                max_height = position.y;
275            }
276            
277            // Calculate drag
278            let velocity_rel = velocity - wind_vector;
279            let velocity_rel_mag = velocity_rel.magnitude();
280            let drag_coefficient = self.calculate_drag_coefficient(velocity_rel_mag);
281            
282            // Calculate drag force
283            let drag_force = 0.5 * air_density * drag_coefficient * 
284                            self.inputs.diameter * self.inputs.diameter * 
285                            std::f64::consts::PI / 4.0 * velocity_rel_mag * velocity_rel_mag;
286            
287            // Calculate acceleration
288            let drag_acceleration = -drag_force / self.inputs.mass;
289            let acceleration = Vector3::new(
290                drag_acceleration * velocity_rel.x / velocity_rel_mag,
291                drag_acceleration * velocity_rel.y / velocity_rel_mag - 9.80665,
292                drag_acceleration * velocity_rel.z / velocity_rel_mag,
293            );
294            
295            // Update state
296            velocity += acceleration * self.time_step;
297            position += velocity * self.time_step;
298            time += self.time_step;
299        }
300        
301        // Get final values
302        let last_point = points.last().ok_or("No trajectory points generated")?;
303        
304        Ok(TrajectoryResult {
305            max_range: last_point.position.x,
306            max_height,
307            time_of_flight: last_point.time,
308            impact_velocity: last_point.velocity_magnitude,
309            impact_energy: last_point.kinetic_energy,
310            points,
311        })
312    }
313    
314    fn calculate_drag_coefficient(&self, velocity: f64) -> f64 {
315        // Simplified drag calculation
316        // In reality, this would use the drag tables based on drag_model
317        let mach = velocity / 343.0; // Approximate speed of sound
318        
319        // Basic drag curve approximation
320        let base_cd = match self.inputs.drag_model {
321            DragModel::G1 => 0.5,
322            DragModel::G7 => 0.4,
323            _ => 0.45,
324        };
325        
326        // Transonic effects
327        if mach > 0.8 && mach < 1.2 {
328            base_cd * 1.5
329        } else if mach > 1.2 {
330            base_cd * 0.8
331        } else {
332            base_cd
333        }
334    }
335}
336
337// Monte Carlo parameters
338#[derive(Debug, Clone)]
339pub struct MonteCarloParams {
340    pub num_simulations: usize,
341    pub velocity_std_dev: f64,
342    pub angle_std_dev: f64,
343    pub bc_std_dev: f64,
344    pub wind_speed_std_dev: f64,
345    pub target_distance: Option<f64>,
346}
347
348impl Default for MonteCarloParams {
349    fn default() -> Self {
350        Self {
351            num_simulations: 1000,
352            velocity_std_dev: 1.0,
353            angle_std_dev: 0.001,
354            bc_std_dev: 0.01,
355            wind_speed_std_dev: 1.0,
356            target_distance: None,
357        }
358    }
359}
360
361// Monte Carlo results
362#[derive(Debug, Clone)]
363pub struct MonteCarloResults {
364    pub ranges: Vec<f64>,
365    pub impact_velocities: Vec<f64>,
366    pub impact_positions: Vec<Vector3<f64>>,
367}
368
369// Run Monte Carlo simulation
370pub fn run_monte_carlo(
371    base_inputs: BallisticInputs,
372    params: MonteCarloParams,
373) -> Result<MonteCarloResults, BallisticsError> {
374    use rand::{thread_rng, Rng};
375    use rand_distr::{Distribution, Normal};
376    
377    let mut rng = thread_rng();
378    let mut ranges = Vec::new();
379    let mut impact_velocities = Vec::new();
380    let mut impact_positions = Vec::new();
381    
382    // Create normal distributions for variations
383    let velocity_dist = Normal::new(base_inputs.muzzle_velocity, params.velocity_std_dev)
384        .map_err(|e| format!("Invalid velocity distribution: {}", e))?;
385    let angle_dist = Normal::new(base_inputs.launch_angle, params.angle_std_dev)
386        .map_err(|e| format!("Invalid angle distribution: {}", e))?;
387    let bc_dist = Normal::new(base_inputs.ballistic_coefficient, params.bc_std_dev)
388        .map_err(|e| format!("Invalid BC distribution: {}", e))?;
389    let wind_dist = Normal::new(0.0, params.wind_speed_std_dev)
390        .map_err(|e| format!("Invalid wind distribution: {}", e))?;
391    
392    for _ in 0..params.num_simulations {
393        // Create varied inputs
394        let mut inputs = base_inputs.clone();
395        inputs.muzzle_velocity = velocity_dist.sample(&mut rng).max(0.0);
396        inputs.launch_angle = angle_dist.sample(&mut rng);
397        inputs.ballistic_coefficient = bc_dist.sample(&mut rng).max(0.01);
398        
399        // Create varied wind
400        let wind = WindConditions {
401            speed: wind_dist.sample(&mut rng).abs(),
402            direction: rng.gen_range(0.0..2.0 * std::f64::consts::PI),
403        };
404        
405        // Run trajectory
406        let solver = TrajectorySolver::new(inputs, wind, Default::default());
407        match solver.solve() {
408            Ok(result) => {
409                ranges.push(result.max_range);
410                impact_velocities.push(result.impact_velocity);
411                if let Some(last_point) = result.points.last() {
412                    impact_positions.push(last_point.position.clone());
413                }
414            },
415            Err(_) => {
416                // Skip failed simulations
417                continue;
418            }
419        }
420    }
421    
422    if ranges.is_empty() {
423        return Err("No successful simulations".into());
424    }
425    
426    Ok(MonteCarloResults {
427        ranges,
428        impact_velocities,
429        impact_positions,
430    })
431}
432
433// Calculate zero angle for a target
434pub fn calculate_zero_angle(
435    inputs: BallisticInputs,
436    target_distance: f64,
437    target_height: f64,
438) -> Result<f64, BallisticsError> {
439    calculate_zero_angle_with_conditions(
440        inputs,
441        target_distance,
442        target_height,
443        WindConditions::default(),
444        AtmosphericConditions::default(),
445    )
446}
447
448pub fn calculate_zero_angle_with_conditions(
449    inputs: BallisticInputs,
450    target_distance: f64,
451    target_height: f64,
452    wind: WindConditions,
453    atmosphere: AtmosphericConditions,
454) -> Result<f64, BallisticsError> {
455    // Binary search for the angle that hits the target
456    let mut low_angle = -0.2; // radians (about -11 degrees)
457    let mut high_angle = 0.2;  // radians (about 11 degrees)
458    let tolerance = 0.00001;   // radians
459    let max_iterations = 50;
460    
461    for iteration in 0..max_iterations {
462        let mid_angle = (low_angle + high_angle) / 2.0;
463        
464        let mut test_inputs = inputs.clone();
465        test_inputs.launch_angle = mid_angle;
466        
467        let mut solver = TrajectorySolver::new(test_inputs, wind.clone(), atmosphere.clone());
468        // Make sure we calculate far enough to reach the target
469        solver.set_max_range(target_distance * 2.0);
470        solver.set_time_step(0.001);
471        let result = solver.solve()?;
472        
473        eprintln!("  Iteration {}: angle = {:.6} rad ({:.4} deg)", 
474                 iteration, mid_angle, mid_angle * 180.0 / std::f64::consts::PI);
475        
476        // Find the height at target distance
477        let mut height_at_target = None;
478        for i in 0..result.points.len() {
479            if result.points[i].position.x >= target_distance {
480                if i > 0 {
481                    // Linear interpolation
482                    let p1 = &result.points[i - 1];
483                    let p2 = &result.points[i];
484                    let t = (target_distance - p1.position.x) / (p2.position.x - p1.position.x);
485                    height_at_target = Some(p1.position.y + t * (p2.position.y - p1.position.y));
486                } else {
487                    height_at_target = Some(result.points[i].position.y);
488                }
489                break;
490            }
491        }
492        
493        match height_at_target {
494            Some(height) => {
495                let error = height - target_height;
496                eprintln!("    Height at target: {:.6} m, target: {:.6} m, error: {:.6} m", 
497                         height, target_height, error);
498                if error.abs() < 0.001 {
499                    eprintln!("  Converged!");
500                    return Ok(mid_angle);
501                }
502                
503                if error > 0.0 {
504                    high_angle = mid_angle;
505                } else {
506                    low_angle = mid_angle;
507                }
508            },
509            None => {
510                // Trajectory didn't reach target distance, increase angle
511                low_angle = mid_angle;
512            }
513        }
514        
515        if (high_angle - low_angle).abs() < tolerance {
516            return Ok(mid_angle);
517        }
518    }
519    
520    Err("Failed to find zero angle".into())
521}
522
523// Estimate BC from trajectory data
524pub fn estimate_bc_from_trajectory(
525    velocity: f64,
526    mass: f64,
527    diameter: f64,
528    points: &[(f64, f64)], // (distance, drop) pairs
529) -> Result<f64, BallisticsError> {
530    // Simple BC estimation using least squares
531    let mut best_bc = 0.5;
532    let mut best_error = f64::MAX;
533    
534    // Try different BC values
535    for bc in (100..1000).step_by(10) {
536        let bc_value = bc as f64 / 1000.0;
537        
538        let inputs = BallisticInputs {
539            muzzle_velocity: velocity,
540            ballistic_coefficient: bc_value,
541            mass,
542            diameter,
543            ..Default::default()
544        };
545        
546        let solver = TrajectorySolver::new(inputs, Default::default(), Default::default());
547        let result = solver.solve()?;
548        
549        // Calculate error
550        let mut total_error = 0.0;
551        for (target_dist, target_drop) in points {
552            // Find drop at this distance
553            let mut calculated_drop = None;
554            for i in 0..result.points.len() {
555                if result.points[i].position.x >= *target_dist {
556                    if i > 0 {
557                        // Linear interpolation
558                        let p1 = &result.points[i - 1];
559                        let p2 = &result.points[i];
560                        let t = (target_dist - p1.position.x) / (p2.position.x - p1.position.x);
561                        calculated_drop = Some(-(p1.position.y + t * (p2.position.y - p1.position.y)));
562                    } else {
563                        calculated_drop = Some(-result.points[i].position.y);
564                    }
565                    break;
566                }
567            }
568            
569            if let Some(drop) = calculated_drop {
570                let error = (drop - target_drop).abs();
571                total_error += error * error;
572            }
573        }
574        
575        if total_error < best_error {
576            best_error = total_error;
577            best_bc = bc_value;
578        }
579    }
580    
581    Ok(best_bc)
582}
583
584// Helper function to calculate air density
585fn calculate_air_density(atmosphere: &AtmosphericConditions) -> f64 {
586    // Simplified air density calculation
587    // P / (R * T) where R is specific gas constant for dry air
588    let r_specific = 287.058; // J/(kg·K)
589    let temperature_k = atmosphere.temperature + 273.15;
590    
591    // Convert pressure from hPa to Pa
592    let pressure_pa = atmosphere.pressure * 100.0;
593    
594    // Basic density calculation
595    let density = pressure_pa / (r_specific * temperature_k);
596    
597    // Altitude correction (simplified)
598    let altitude_factor = (-atmosphere.altitude / 8000.0).exp();
599    
600    density * altitude_factor
601}
602
603// Add rand dependencies for Monte Carlo
604use rand;
605use rand_distr;