ballistics_engine/
cli_api.rs

1// CLI API module - provides simplified interfaces for command-line tool
2use crate::DragModel;
3use crate::wind_shear::{WindShearProfile, WindShearModel, WindLayer};
4use crate::transonic_drag::{transonic_correction, get_projectile_shape, ProjectileShape};
5use crate::trajectory_sampling::{sample_trajectory, TrajectoryData, TrajectoryOutputs, TrajectorySample};
6use crate::pitch_damping::{calculate_pitch_damping_coefficient, PitchDampingCoefficients};
7use crate::precession_nutation::{AngularState, PrecessionNutationParams, calculate_combined_angular_motion};
8use nalgebra::Vector3;
9use std::error::Error;
10use std::fmt;
11
12// Error type for CLI operations
13#[derive(Debug)]
14pub struct BallisticsError {
15    message: String,
16}
17
18impl fmt::Display for BallisticsError {
19    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
20        write!(f, "{}", self.message)
21    }
22}
23
24impl Error for BallisticsError {}
25
26impl From<String> for BallisticsError {
27    fn from(msg: String) -> Self {
28        BallisticsError { message: msg }
29    }
30}
31
32impl From<&str> for BallisticsError {
33    fn from(msg: &str) -> Self {
34        BallisticsError { message: msg.to_string() }
35    }
36}
37
38// Ballistic input parameters
39#[derive(Debug, Clone)]
40pub struct BallisticInputs {
41    // Core ballistics parameters
42    pub muzzle_velocity: f64,      // m/s
43    pub launch_angle: f64,          // radians (same as muzzle_angle)
44    pub ballistic_coefficient: f64,
45    pub mass: f64,                  // kg
46    pub diameter: f64,              // meters
47    pub drag_model: DragModel,
48    pub sight_height: f64,          // meters
49    
50    // Additional fields for compatibility
51    pub altitude: f64,              // meters
52    pub bc_type: DragModel,         // same as drag_model
53    pub bc_value: f64,              // same as ballistic_coefficient
54    pub caliber_inches: f64,        // diameter in inches
55    pub weight_grains: f64,         // mass in grains
56    pub bullet_diameter: f64,       // same as diameter
57    pub bullet_mass: f64,           // same as mass
58    pub bullet_length: f64,         // meters
59    pub muzzle_angle: f64,          // same as launch_angle
60    pub target_distance: f64,       // meters
61    pub azimuth_angle: f64,         // horizontal aiming angle in radians
62    pub use_rk4: bool,              // Use RK4 integration instead of Euler
63    pub temperature: f64,           // Celsius
64    pub twist_rate: f64,            // inches per turn
65    pub is_twist_right: bool,       // right-hand twist
66    pub shooting_angle: f64,        // uphill/downhill angle in radians
67    pub latitude: Option<f64>,      // degrees
68    pub ground_threshold: f64,      // meters below which to stop
69    
70    // Advanced effects flags
71    pub enable_advanced_effects: bool,
72    pub use_powder_sensitivity: bool,
73    pub powder_temp_sensitivity: f64,
74    pub powder_temp: f64,          // Celsius
75    pub tipoff_yaw: f64,            // radians
76    pub tipoff_decay_distance: f64, // meters
77    pub use_bc_segments: bool,
78    pub bc_segments: Option<Vec<(f64, f64)>>,  // Mach-BC pairs
79    pub bc_segments_data: Option<Vec<crate::BCSegmentData>>,  // Velocity-BC segments
80    pub use_enhanced_spin_drift: bool,
81    pub use_form_factor: bool,
82    pub enable_wind_shear: bool,
83    pub wind_shear_model: String,
84    pub enable_trajectory_sampling: bool,
85    pub sample_interval: f64,  // meters
86    pub enable_pitch_damping: bool,
87    pub enable_precession_nutation: bool,
88    
89    // Additional data fields
90    pub bc_type_str: Option<String>,
91    pub bullet_model: Option<String>,
92    pub bullet_id: Option<String>,
93}
94
95impl Default for BallisticInputs {
96    fn default() -> Self {
97        let mass_kg = 0.01;
98        let diameter_m = 0.00762;
99        let bc = 0.5;
100        let launch_angle_rad = 0.0;
101        let drag_model = DragModel::G1;
102        
103        Self {
104            // Core parameters
105            muzzle_velocity: 800.0,
106            launch_angle: launch_angle_rad,
107            ballistic_coefficient: bc,
108            mass: mass_kg,
109            diameter: diameter_m,
110            drag_model,
111            sight_height: 0.05,
112            
113            // Duplicate fields for compatibility
114            altitude: 0.0,
115            bc_type: drag_model,
116            bc_value: bc,
117            caliber_inches: diameter_m / 0.0254,  // Convert to inches
118            weight_grains: mass_kg / 0.00006479891,  // Convert to grains
119            bullet_diameter: diameter_m,
120            bullet_mass: mass_kg,
121            bullet_length: diameter_m * 4.0,  // Approximate
122            muzzle_angle: launch_angle_rad,
123            target_distance: 100.0,
124            azimuth_angle: 0.0,
125            use_rk4: true,  // Default to RK4 for better accuracy
126            temperature: 15.0,
127            twist_rate: 12.0,  // 1:12" typical
128            is_twist_right: true,
129            shooting_angle: 0.0,
130            latitude: None,
131            ground_threshold: -10.0,
132            
133            // Advanced effects (disabled by default)
134            enable_advanced_effects: false,
135            use_powder_sensitivity: false,
136            powder_temp_sensitivity: 0.0,
137            powder_temp: 15.0,
138            tipoff_yaw: 0.0,
139            tipoff_decay_distance: 50.0,
140            use_bc_segments: false,
141            bc_segments: None,
142            bc_segments_data: None,
143            use_enhanced_spin_drift: false,
144            use_form_factor: false,
145            enable_wind_shear: false,
146            wind_shear_model: "none".to_string(),
147            enable_trajectory_sampling: false,
148            sample_interval: 10.0,  // Default 10 meter intervals
149            enable_pitch_damping: false,
150            enable_precession_nutation: false,
151            
152            // Optional data
153            bc_type_str: None,
154            bullet_model: None,
155            bullet_id: None,
156        }
157    }
158}
159
160// Wind conditions
161#[derive(Debug, Clone)]
162pub struct WindConditions {
163    pub speed: f64,        // m/s
164    pub direction: f64,    // radians (0 = North, PI/2 = East)
165}
166
167impl Default for WindConditions {
168    fn default() -> Self {
169        Self {
170            speed: 0.0,
171            direction: 0.0,
172        }
173    }
174}
175
176// Atmospheric conditions
177#[derive(Debug, Clone)]
178pub struct AtmosphericConditions {
179    pub temperature: f64,  // Celsius
180    pub pressure: f64,     // hPa
181    pub humidity: f64,     // percentage (0-100)
182    pub altitude: f64,     // meters
183}
184
185impl Default for AtmosphericConditions {
186    fn default() -> Self {
187        Self {
188            temperature: 15.0,
189            pressure: 1013.25,
190            humidity: 50.0,
191            altitude: 0.0,
192        }
193    }
194}
195
196// Trajectory point data
197#[derive(Debug, Clone)]
198pub struct TrajectoryPoint {
199    pub time: f64,
200    pub position: Vector3<f64>,
201    pub velocity_magnitude: f64,
202    pub kinetic_energy: f64,
203}
204
205// Trajectory result
206#[derive(Debug, Clone)]
207pub struct TrajectoryResult {
208    pub max_range: f64,
209    pub max_height: f64,
210    pub time_of_flight: f64,
211    pub impact_velocity: f64,
212    pub impact_energy: f64,
213    pub points: Vec<TrajectoryPoint>,
214    pub sampled_points: Option<Vec<TrajectorySample>>,  // Trajectory samples at regular intervals
215    pub min_pitch_damping: Option<f64>,  // Minimum pitch damping coefficient (for stability warning)
216    pub transonic_mach: Option<f64>,      // Mach number when entering transonic regime
217    pub angular_state: Option<AngularState>,  // Final angular state if precession/nutation enabled
218    pub max_yaw_angle: Option<f64>,           // Maximum yaw angle during flight (radians)
219    pub max_precession_angle: Option<f64>,    // Maximum precession angle (radians)
220}
221
222// Trajectory solver
223pub struct TrajectorySolver {
224    inputs: BallisticInputs,
225    wind: WindConditions,
226    atmosphere: AtmosphericConditions,
227    max_range: f64,
228    time_step: f64,
229}
230
231impl TrajectorySolver {
232    pub fn new(mut inputs: BallisticInputs, wind: WindConditions, atmosphere: AtmosphericConditions) -> Self {
233        // Ensure duplicate fields are synchronized
234        inputs.bc_type = inputs.drag_model;
235        inputs.bc_value = inputs.ballistic_coefficient;
236        inputs.bullet_diameter = inputs.diameter;
237        inputs.bullet_mass = inputs.mass;
238        inputs.muzzle_angle = inputs.launch_angle;
239        inputs.caliber_inches = inputs.diameter / 0.0254;
240        inputs.weight_grains = inputs.mass / 0.00006479891;
241        
242        Self {
243            inputs,
244            wind,
245            atmosphere,
246            max_range: 1000.0,
247            time_step: 0.001,
248        }
249    }
250    
251    pub fn set_max_range(&mut self, range: f64) {
252        self.max_range = range;
253    }
254    
255    pub fn set_time_step(&mut self, step: f64) {
256        self.time_step = step;
257    }
258    
259    fn get_wind_at_altitude(&self, altitude_m: f64) -> Vector3<f64> {
260        // Create wind shear profile based on surface wind
261        let profile = WindShearProfile {
262            model: if self.inputs.wind_shear_model == "logarithmic" {
263                WindShearModel::Logarithmic
264            } else if self.inputs.wind_shear_model == "power" {
265                WindShearModel::PowerLaw
266            } else {
267                WindShearModel::PowerLaw  // Default to power law
268            },
269            surface_wind: WindLayer {
270                altitude_m: 0.0,
271                speed_mps: self.wind.speed,
272                direction_deg: self.wind.direction.to_degrees(),
273            },
274            reference_height: 10.0,  // Standard meteorological measurement height
275            roughness_length: 0.03,  // Short grass
276            power_exponent: 1.0 / 7.0,  // Neutral stability
277            geostrophic_wind: None,
278            custom_layers: Vec::new(),
279        };
280        
281        profile.get_wind_at_altitude(altitude_m)
282    }
283    
284    pub fn solve(&self) -> Result<TrajectoryResult, BallisticsError> {
285        if self.inputs.use_rk4 {
286            self.solve_rk4()
287        } else {
288            self.solve_euler()
289        }
290    }
291    
292    fn solve_euler(&self) -> Result<TrajectoryResult, BallisticsError> {
293        // Simple trajectory integration using Euler method
294        let mut time = 0.0;
295        let mut position = Vector3::new(0.0, self.inputs.sight_height, 0.0);
296        // Calculate initial velocity components with both elevation and azimuth
297        let horizontal_velocity = self.inputs.muzzle_velocity * self.inputs.launch_angle.cos();
298        let mut velocity = Vector3::new(
299            horizontal_velocity * self.inputs.azimuth_angle.sin(),  // X: side deviation (left/right)
300            self.inputs.muzzle_velocity * self.inputs.launch_angle.sin(),  // Y: vertical component
301            horizontal_velocity * self.inputs.azimuth_angle.cos(),  // Z: forward component (down-range)
302        );
303        
304        let mut points = Vec::new();
305        let mut max_height = position.y;
306        let mut min_pitch_damping = 1.0;  // Track minimum pitch damping coefficient
307        let mut transonic_mach = None;    // Track when we enter transonic
308        
309        // Initialize angular state for precession/nutation tracking
310        let mut angular_state = if self.inputs.enable_precession_nutation {
311            Some(AngularState {
312                pitch_angle: 0.001,  // Small initial disturbance
313                yaw_angle: 0.001,
314                pitch_rate: 0.0,
315                yaw_rate: 0.0,
316                precession_angle: 0.0,
317                nutation_phase: 0.0,
318            })
319        } else {
320            None
321        };
322        let mut max_yaw_angle = 0.0;
323        let mut max_precession_angle = 0.0;
324        
325        // Calculate air density
326        let air_density = calculate_air_density(&self.atmosphere);
327        
328        // Wind vector
329        let wind_vector = Vector3::new(
330            self.wind.speed * self.wind.direction.sin(),
331            0.0,
332            self.wind.speed * self.wind.direction.cos(),
333        );
334        
335        // Main integration loop
336        while position.z < self.max_range && position.y >= 0.0 && time < 100.0 {
337            // Store trajectory point
338            let velocity_magnitude = velocity.magnitude();
339            let kinetic_energy = 0.5 * self.inputs.mass * velocity_magnitude * velocity_magnitude;
340            
341            points.push(TrajectoryPoint {
342                time,
343                position: position.clone(),
344                velocity_magnitude,
345                kinetic_energy,
346            });
347            
348            // Track max height
349            if position.y > max_height {
350                max_height = position.y;
351            }
352            
353            // Calculate pitch damping if enabled
354            if self.inputs.enable_pitch_damping {
355                let temp_c = self.atmosphere.temperature;
356                let temp_k = temp_c + 273.15;
357                let speed_of_sound = (1.4 * 287.05 * temp_k).sqrt();
358                let mach = velocity_magnitude / speed_of_sound;
359                
360                // Track when we enter transonic
361                if transonic_mach.is_none() && mach < 1.2 && mach > 0.8 {
362                    transonic_mach = Some(mach);
363                }
364                
365                // Calculate pitch damping coefficient
366                let bullet_type = if let Some(ref model) = self.inputs.bullet_model {
367                    model.as_str()
368                } else {
369                    "default"
370                };
371                let coeffs = PitchDampingCoefficients::from_bullet_type(bullet_type);
372                let pitch_damping = calculate_pitch_damping_coefficient(mach, &coeffs);
373                
374                // Track minimum (most critical for stability)
375                if pitch_damping < min_pitch_damping {
376                    min_pitch_damping = pitch_damping;
377                }
378            }
379            
380            // Calculate precession/nutation if enabled
381            if self.inputs.enable_precession_nutation {
382                if let Some(ref mut state) = angular_state {
383                    let velocity_magnitude = velocity.magnitude();
384                    let temp_c = self.atmosphere.temperature;
385                    let temp_k = temp_c + 273.15;
386                    let speed_of_sound = (1.4 * 287.05 * temp_k).sqrt();
387                    let mach = velocity_magnitude / speed_of_sound;
388                    
389                    // Calculate spin rate from twist rate and velocity
390                    let spin_rate_rad_s = if self.inputs.twist_rate > 0.0 {
391                        let velocity_fps = velocity_magnitude * 3.28084;
392                        let twist_rate_ft = self.inputs.twist_rate / 12.0;
393                        (velocity_fps / twist_rate_ft) * 2.0 * std::f64::consts::PI
394                    } else {
395                        0.0
396                    };
397                    
398                    // Create precession/nutation parameters
399                    let params = PrecessionNutationParams {
400                        mass_kg: self.inputs.mass,
401                        caliber_m: self.inputs.diameter,
402                        length_m: self.inputs.bullet_length,
403                        spin_rate_rad_s,
404                        spin_inertia: 6.94e-8,      // Typical value
405                        transverse_inertia: 9.13e-7, // Typical value
406                        velocity_mps: velocity_magnitude,
407                        air_density_kg_m3: air_density,
408                        mach,
409                        pitch_damping_coeff: -0.8,
410                        nutation_damping_factor: 0.05,
411                    };
412                    
413                    // Update angular state
414                    *state = calculate_combined_angular_motion(
415                        &params,
416                        state,
417                        time,
418                        self.time_step,
419                        0.001,  // Initial disturbance
420                    );
421                    
422                    // Track maximums
423                    if state.yaw_angle.abs() > max_yaw_angle {
424                        max_yaw_angle = state.yaw_angle.abs();
425                    }
426                    if state.precession_angle.abs() > max_precession_angle {
427                        max_precession_angle = state.precession_angle.abs();
428                    }
429                }
430            }
431            
432            // Calculate drag with altitude-dependent wind if enabled
433            let actual_wind = if self.inputs.enable_wind_shear {
434                self.get_wind_at_altitude(position.y)
435            } else {
436                wind_vector.clone()
437            };
438            let velocity_rel = velocity - actual_wind;
439            let velocity_rel_mag = velocity_rel.magnitude();
440            let drag_coefficient = self.calculate_drag_coefficient(velocity_rel_mag);
441            
442            // Calculate drag force
443            let drag_force = 0.5 * air_density * drag_coefficient * 
444                            self.inputs.diameter * self.inputs.diameter * 
445                            std::f64::consts::PI / 4.0 * velocity_rel_mag * velocity_rel_mag;
446            
447            // Calculate acceleration
448            let drag_acceleration = -drag_force / self.inputs.mass;
449            let acceleration = Vector3::new(
450                drag_acceleration * velocity_rel.x / velocity_rel_mag,
451                drag_acceleration * velocity_rel.y / velocity_rel_mag - 9.80665,
452                drag_acceleration * velocity_rel.z / velocity_rel_mag,
453            );
454            
455            // Update state
456            velocity += acceleration * self.time_step;
457            position += velocity * self.time_step;
458            time += self.time_step;
459        }
460        
461        // Get final values
462        let last_point = points.last().ok_or("No trajectory points generated")?;
463        
464        // Create trajectory sampling data if enabled
465        let sampled_points = if self.inputs.enable_trajectory_sampling {
466            let trajectory_data = TrajectoryData {
467                times: points.iter().map(|p| p.time).collect(),
468                positions: points.iter().map(|p| p.position.clone()).collect(),
469                velocities: points.iter().map(|p| {
470                    // Reconstruct velocity vectors from magnitude (approximate)
471                    Vector3::new(0.0, 0.0, p.velocity_magnitude)
472                }).collect(),
473                transonic_distances: Vec::new(),  // TODO: Track Mach transitions
474            };
475            
476            let outputs = TrajectoryOutputs {
477                target_distance_horiz_m: last_point.position.z,
478                target_vertical_height_m: last_point.position.y,
479                time_of_flight_s: last_point.time,
480                max_ord_dist_horiz_m: max_height,
481            };
482            
483            // Sample at specified intervals
484            let samples = sample_trajectory(&trajectory_data, &outputs, self.inputs.sample_interval, self.inputs.mass);
485            Some(samples)
486        } else {
487            None
488        };
489        
490        Ok(TrajectoryResult {
491            max_range: last_point.position.z,
492            max_height,
493            time_of_flight: last_point.time,
494            impact_velocity: last_point.velocity_magnitude,
495            impact_energy: last_point.kinetic_energy,
496            points,
497            sampled_points,
498            min_pitch_damping: if self.inputs.enable_pitch_damping { Some(min_pitch_damping) } else { None },
499            transonic_mach: transonic_mach,
500            angular_state: angular_state,
501            max_yaw_angle: if self.inputs.enable_precession_nutation { Some(max_yaw_angle) } else { None },
502            max_precession_angle: if self.inputs.enable_precession_nutation { Some(max_precession_angle) } else { None },
503        })
504    }
505    
506    fn solve_rk4(&self) -> Result<TrajectoryResult, BallisticsError> {
507        // RK4 trajectory integration for better accuracy
508        let mut time = 0.0;
509        let mut position = Vector3::new(0.0, self.inputs.sight_height, 0.0);
510        
511        // Calculate initial velocity components with both elevation and azimuth
512        let horizontal_velocity = self.inputs.muzzle_velocity * self.inputs.launch_angle.cos();
513        let mut velocity = Vector3::new(
514            horizontal_velocity * self.inputs.azimuth_angle.sin(),
515            self.inputs.muzzle_velocity * self.inputs.launch_angle.sin(),
516            horizontal_velocity * self.inputs.azimuth_angle.cos(),
517        );
518        
519        let mut points = Vec::new();
520        let mut max_height = position.y;
521        let mut min_pitch_damping = 1.0;  // Track minimum pitch damping coefficient
522        let mut transonic_mach = None;    // Track when we enter transonic
523        
524        // Initialize angular state for precession/nutation tracking
525        let mut angular_state = if self.inputs.enable_precession_nutation {
526            Some(AngularState {
527                pitch_angle: 0.001,  // Small initial disturbance
528                yaw_angle: 0.001,
529                pitch_rate: 0.0,
530                yaw_rate: 0.0,
531                precession_angle: 0.0,
532                nutation_phase: 0.0,
533            })
534        } else {
535            None
536        };
537        let mut max_yaw_angle = 0.0;
538        let mut max_precession_angle = 0.0;
539        
540        // Calculate air density
541        let air_density = calculate_air_density(&self.atmosphere);
542        
543        // Wind vector
544        let wind_vector = Vector3::new(
545            self.wind.speed * self.wind.direction.sin(),
546            0.0,
547            self.wind.speed * self.wind.direction.cos(),
548        );
549        
550        // Main RK4 integration loop
551        while position.z < self.max_range && position.y >= 0.0 && time < 100.0 {
552            // Store trajectory point
553            let velocity_magnitude = velocity.magnitude();
554            let kinetic_energy = 0.5 * self.inputs.mass * velocity_magnitude * velocity_magnitude;
555            
556            points.push(TrajectoryPoint {
557                time,
558                position: position.clone(),
559                velocity_magnitude,
560                kinetic_energy,
561            });
562            
563            if position.y > max_height {
564                max_height = position.y;
565            }
566            
567            // Calculate pitch damping if enabled (RK4 solver)
568            if self.inputs.enable_pitch_damping {
569                let temp_c = self.atmosphere.temperature;
570                let temp_k = temp_c + 273.15;
571                let speed_of_sound = (1.4 * 287.05 * temp_k).sqrt();
572                let mach = velocity_magnitude / speed_of_sound;
573                
574                // Track when we enter transonic
575                if transonic_mach.is_none() && mach < 1.2 && mach > 0.8 {
576                    transonic_mach = Some(mach);
577                }
578                
579                // Calculate pitch damping coefficient
580                let bullet_type = if let Some(ref model) = self.inputs.bullet_model {
581                    model.as_str()
582                } else {
583                    "default"
584                };
585                let coeffs = PitchDampingCoefficients::from_bullet_type(bullet_type);
586                let pitch_damping = calculate_pitch_damping_coefficient(mach, &coeffs);
587                
588                // Track minimum (most critical for stability)
589                if pitch_damping < min_pitch_damping {
590                    min_pitch_damping = pitch_damping;
591                }
592            }
593            
594            // Calculate precession/nutation if enabled (RK4 solver)
595            if self.inputs.enable_precession_nutation {
596                if let Some(ref mut state) = angular_state {
597                    let velocity_magnitude = velocity.magnitude();
598                    let temp_c = self.atmosphere.temperature;
599                    let temp_k = temp_c + 273.15;
600                    let speed_of_sound = (1.4 * 287.05 * temp_k).sqrt();
601                    let mach = velocity_magnitude / speed_of_sound;
602                    
603                    // Calculate spin rate from twist rate and velocity
604                    let spin_rate_rad_s = if self.inputs.twist_rate > 0.0 {
605                        let velocity_fps = velocity_magnitude * 3.28084;
606                        let twist_rate_ft = self.inputs.twist_rate / 12.0;
607                        (velocity_fps / twist_rate_ft) * 2.0 * std::f64::consts::PI
608                    } else {
609                        0.0
610                    };
611                    
612                    // Create precession/nutation parameters
613                    let params = PrecessionNutationParams {
614                        mass_kg: self.inputs.mass,
615                        caliber_m: self.inputs.diameter,
616                        length_m: self.inputs.bullet_length,
617                        spin_rate_rad_s,
618                        spin_inertia: 6.94e-8,      // Typical value
619                        transverse_inertia: 9.13e-7, // Typical value
620                        velocity_mps: velocity_magnitude,
621                        air_density_kg_m3: air_density,
622                        mach,
623                        pitch_damping_coeff: -0.8,
624                        nutation_damping_factor: 0.05,
625                    };
626                    
627                    // Update angular state
628                    *state = calculate_combined_angular_motion(
629                        &params,
630                        state,
631                        time,
632                        self.time_step,
633                        0.001,  // Initial disturbance
634                    );
635                    
636                    // Track maximums
637                    if state.yaw_angle.abs() > max_yaw_angle {
638                        max_yaw_angle = state.yaw_angle.abs();
639                    }
640                    if state.precession_angle.abs() > max_precession_angle {
641                        max_precession_angle = state.precession_angle.abs();
642                    }
643                }
644            }
645            
646            // RK4 method
647            let dt = self.time_step;
648            
649            // k1
650            let acc1 = self.calculate_acceleration(&position, &velocity, air_density, &wind_vector);
651            
652            // k2
653            let pos2 = position + velocity * (dt * 0.5);
654            let vel2 = velocity + acc1 * (dt * 0.5);
655            let acc2 = self.calculate_acceleration(&pos2, &vel2, air_density, &wind_vector);
656            
657            // k3
658            let pos3 = position + vel2 * (dt * 0.5);
659            let vel3 = velocity + acc2 * (dt * 0.5);
660            let acc3 = self.calculate_acceleration(&pos3, &vel3, air_density, &wind_vector);
661            
662            // k4
663            let pos4 = position + vel3 * dt;
664            let vel4 = velocity + acc3 * dt;
665            let acc4 = self.calculate_acceleration(&pos4, &vel4, air_density, &wind_vector);
666            
667            // Update position and velocity
668            position += (velocity + vel2 * 2.0 + vel3 * 2.0 + vel4) * (dt / 6.0);
669            velocity += (acc1 + acc2 * 2.0 + acc3 * 2.0 + acc4) * (dt / 6.0);
670            time += dt;
671        }
672        
673        // Get final values
674        let last_point = points.last().ok_or("No trajectory points generated")?;
675        
676        // Create trajectory sampling data if enabled
677        let sampled_points = if self.inputs.enable_trajectory_sampling {
678            let trajectory_data = TrajectoryData {
679                times: points.iter().map(|p| p.time).collect(),
680                positions: points.iter().map(|p| p.position.clone()).collect(),
681                velocities: points.iter().map(|p| {
682                    // Reconstruct velocity vectors from magnitude (approximate)
683                    Vector3::new(0.0, 0.0, p.velocity_magnitude)
684                }).collect(),
685                transonic_distances: Vec::new(),  // TODO: Track Mach transitions
686            };
687            
688            let outputs = TrajectoryOutputs {
689                target_distance_horiz_m: last_point.position.z,
690                target_vertical_height_m: last_point.position.y,
691                time_of_flight_s: last_point.time,
692                max_ord_dist_horiz_m: max_height,
693            };
694            
695            // Sample at specified intervals
696            let samples = sample_trajectory(&trajectory_data, &outputs, self.inputs.sample_interval, self.inputs.mass);
697            Some(samples)
698        } else {
699            None
700        };
701        
702        Ok(TrajectoryResult {
703            max_range: last_point.position.z,
704            max_height,
705            time_of_flight: last_point.time,
706            impact_velocity: last_point.velocity_magnitude,
707            impact_energy: last_point.kinetic_energy,
708            points,
709            sampled_points,
710            min_pitch_damping: if self.inputs.enable_pitch_damping { Some(min_pitch_damping) } else { None },
711            transonic_mach: transonic_mach,
712            angular_state: angular_state,
713            max_yaw_angle: if self.inputs.enable_precession_nutation { Some(max_yaw_angle) } else { None },
714            max_precession_angle: if self.inputs.enable_precession_nutation { Some(max_precession_angle) } else { None },
715        })
716    }
717    
718    fn calculate_acceleration(&self, position: &Vector3<f64>, velocity: &Vector3<f64>, air_density: f64, wind_vector: &Vector3<f64>) -> Vector3<f64> {
719        // Calculate altitude-dependent wind if wind shear is enabled
720        let actual_wind = if self.inputs.enable_wind_shear {
721            self.get_wind_at_altitude(position.y)
722        } else {
723            wind_vector.clone()
724        };
725        
726        let relative_velocity = velocity - &actual_wind;
727        let velocity_magnitude = relative_velocity.magnitude();
728        
729        if velocity_magnitude < 0.001 {
730            return Vector3::new(0.0, -9.81, 0.0);
731        }
732        
733        // Calculate drag force
734        let cd = self.calculate_drag_coefficient(velocity_magnitude);
735        let reference_area = std::f64::consts::PI * (self.inputs.diameter / 2.0).powi(2);
736        let drag_magnitude = 0.5 * air_density * velocity_magnitude.powi(2) * cd * reference_area / self.inputs.ballistic_coefficient;
737        
738        // Drag acts opposite to velocity
739        let drag_force = -relative_velocity.normalize() * drag_magnitude;
740        
741        // Total acceleration = drag/mass + gravity
742        let acceleration = drag_force / self.inputs.mass + Vector3::new(0.0, -9.81, 0.0);
743        
744        acceleration
745    }
746    
747    fn calculate_drag_coefficient(&self, velocity: f64) -> f64 {
748        // Calculate speed of sound based on atmospheric temperature
749        // Use standard atmosphere temperature at sea level if not available
750        let temp_c = self.atmosphere.temperature;
751        let temp_k = temp_c + 273.15;
752        let gamma = 1.4;  // Ratio of specific heats for air
753        let r_specific = 287.05;  // Specific gas constant for air (J/kg·K)
754        let speed_of_sound = (gamma * r_specific * temp_k).sqrt();
755        let mach = velocity / speed_of_sound;
756        
757        // Base drag coefficient from drag model
758        let base_cd = match self.inputs.drag_model {
759            DragModel::G1 => 0.5,
760            DragModel::G7 => 0.4,
761            _ => 0.45,
762        };
763        
764        // Determine projectile shape for transonic corrections
765        let projectile_shape = if let Some(ref model) = self.inputs.bullet_model {
766            // Try to determine shape from bullet model string
767            if model.to_lowercase().contains("boat") || model.to_lowercase().contains("bt") {
768                ProjectileShape::BoatTail
769            } else if model.to_lowercase().contains("round") || model.to_lowercase().contains("rn") {
770                ProjectileShape::RoundNose
771            } else if model.to_lowercase().contains("flat") || model.to_lowercase().contains("fb") {
772                ProjectileShape::FlatBase
773            } else {
774                // Use heuristic based on caliber, weight, and drag model
775                get_projectile_shape(
776                    self.inputs.diameter,
777                    self.inputs.mass / 0.00006479891,  // Convert kg to grains
778                    &self.inputs.drag_model.to_string()
779                )
780            }
781        } else {
782            // Use heuristic based on caliber, weight, and drag model
783            get_projectile_shape(
784                self.inputs.diameter,
785                self.inputs.mass / 0.00006479891,  // Convert kg to grains
786                &self.inputs.drag_model.to_string()
787            )
788        };
789        
790        // Apply transonic corrections
791        // Enable wave drag if advanced effects are enabled
792        let include_wave_drag = self.inputs.enable_advanced_effects;
793        transonic_correction(mach, base_cd, projectile_shape, include_wave_drag)
794    }
795}
796
797// Monte Carlo parameters
798#[derive(Debug, Clone)]
799pub struct MonteCarloParams {
800    pub num_simulations: usize,
801    pub velocity_std_dev: f64,
802    pub angle_std_dev: f64,
803    pub bc_std_dev: f64,
804    pub wind_speed_std_dev: f64,
805    pub target_distance: Option<f64>,
806    pub base_wind_speed: f64,
807    pub base_wind_direction: f64,
808    pub azimuth_std_dev: f64,  // Horizontal aiming variation in radians
809}
810
811impl Default for MonteCarloParams {
812    fn default() -> Self {
813        Self {
814            num_simulations: 1000,
815            velocity_std_dev: 1.0,
816            angle_std_dev: 0.001,
817            bc_std_dev: 0.01,
818            wind_speed_std_dev: 1.0,
819            target_distance: None,
820            base_wind_speed: 0.0,
821            base_wind_direction: 0.0,
822            azimuth_std_dev: 0.001,  // Default horizontal spread ~0.057 degrees
823        }
824    }
825}
826
827// Monte Carlo results
828#[derive(Debug, Clone)]
829pub struct MonteCarloResults {
830    pub ranges: Vec<f64>,
831    pub impact_velocities: Vec<f64>,
832    pub impact_positions: Vec<Vector3<f64>>,
833}
834
835// Run Monte Carlo simulation (backwards compatibility)
836pub fn run_monte_carlo(
837    base_inputs: BallisticInputs,
838    params: MonteCarloParams,
839) -> Result<MonteCarloResults, BallisticsError> {
840    let base_wind = WindConditions {
841        speed: params.base_wind_speed,
842        direction: params.base_wind_direction,
843    };
844    run_monte_carlo_with_wind(base_inputs, base_wind, params)
845}
846
847// Run Monte Carlo simulation with wind
848pub fn run_monte_carlo_with_wind(
849    base_inputs: BallisticInputs,
850    base_wind: WindConditions,
851    params: MonteCarloParams,
852) -> Result<MonteCarloResults, BallisticsError> {
853    use rand::{thread_rng, Rng};
854    use rand_distr::{Distribution, Normal};
855    
856    let mut rng = thread_rng();
857    let mut ranges = Vec::new();
858    let mut impact_velocities = Vec::new();
859    let mut impact_positions = Vec::new();
860    
861    // First, calculate baseline trajectory with no variations
862    let baseline_solver = TrajectorySolver::new(base_inputs.clone(), base_wind.clone(), Default::default());
863    let baseline_result = baseline_solver.solve()?;
864    let baseline_impact = baseline_result.points.last()
865        .ok_or("No baseline trajectory points")?
866        .position.clone();
867    
868    // Create normal distributions for variations
869    let velocity_dist = Normal::new(base_inputs.muzzle_velocity, params.velocity_std_dev)
870        .map_err(|e| format!("Invalid velocity distribution: {}", e))?;
871    let angle_dist = Normal::new(base_inputs.launch_angle, params.angle_std_dev)
872        .map_err(|e| format!("Invalid angle distribution: {}", e))?;
873    let bc_dist = Normal::new(base_inputs.ballistic_coefficient, params.bc_std_dev)
874        .map_err(|e| format!("Invalid BC distribution: {}", e))?;
875    let wind_speed_dist = Normal::new(base_wind.speed, params.wind_speed_std_dev)
876        .map_err(|e| format!("Invalid wind speed distribution: {}", e))?;
877    let wind_dir_dist = Normal::new(base_wind.direction, params.wind_speed_std_dev * 0.1)  // Small variation in direction
878        .map_err(|e| format!("Invalid wind direction distribution: {}", e))?;
879    let azimuth_dist = Normal::new(base_inputs.azimuth_angle, params.azimuth_std_dev)
880        .map_err(|e| format!("Invalid azimuth distribution: {}", e))?;
881    
882    // Create distribution for pointing errors (simulates shooter's aiming consistency)
883    let distance = baseline_impact.z;  // Distance to target
884    let pointing_error_dist = Normal::new(0.0, params.angle_std_dev * distance)
885        .map_err(|e| format!("Invalid pointing distribution: {}", e))?;
886    
887    for _ in 0..params.num_simulations {
888        // Create varied inputs
889        let mut inputs = base_inputs.clone();
890        inputs.muzzle_velocity = velocity_dist.sample(&mut rng).max(0.0);
891        inputs.launch_angle = angle_dist.sample(&mut rng);
892        inputs.ballistic_coefficient = bc_dist.sample(&mut rng).max(0.01);
893        inputs.azimuth_angle = azimuth_dist.sample(&mut rng);  // Add horizontal variation
894        
895        // Create varied wind (now based on base wind conditions)
896        let wind = WindConditions {
897            speed: wind_speed_dist.sample(&mut rng).abs(),
898            direction: wind_dir_dist.sample(&mut rng),
899        };
900        
901        // Run trajectory
902        let solver = TrajectorySolver::new(inputs, wind, Default::default());
903        match solver.solve() {
904            Ok(result) => {
905                ranges.push(result.max_range);
906                impact_velocities.push(result.impact_velocity);
907                if let Some(last_point) = result.points.last() {
908                    // Calculate physical deviation from baseline impact point
909                    let mut deviation = Vector3::new(
910                        last_point.position.x - baseline_impact.x,  // Lateral deviation
911                        last_point.position.y - baseline_impact.y,  // Vertical deviation
912                        last_point.position.z - baseline_impact.z,  // Range deviation
913                    );
914                    
915                    // Add additional pointing error to simulate realistic group sizes
916                    // This represents the shooter's ability to aim consistently
917                    let pointing_error_y = pointing_error_dist.sample(&mut rng);
918                    deviation.y += pointing_error_y;
919                    
920                    impact_positions.push(deviation);
921                }
922            },
923            Err(_) => {
924                // Skip failed simulations
925                continue;
926            }
927        }
928    }
929    
930    if ranges.is_empty() {
931        return Err("No successful simulations".into());
932    }
933    
934    Ok(MonteCarloResults {
935        ranges,
936        impact_velocities,
937        impact_positions,
938    })
939}
940
941// Calculate zero angle for a target
942pub fn calculate_zero_angle(
943    inputs: BallisticInputs,
944    target_distance: f64,
945    target_height: f64,
946) -> Result<f64, BallisticsError> {
947    calculate_zero_angle_with_conditions(
948        inputs,
949        target_distance,
950        target_height,
951        WindConditions::default(),
952        AtmosphericConditions::default(),
953    )
954}
955
956pub fn calculate_zero_angle_with_conditions(
957    inputs: BallisticInputs,
958    target_distance: f64,
959    target_height: f64,
960    wind: WindConditions,
961    atmosphere: AtmosphericConditions,
962) -> Result<f64, BallisticsError> {
963    // Binary search for the angle that hits the target
964    let mut low_angle = -0.2; // radians (about -11 degrees)
965    let mut high_angle = 0.2;  // radians (about 11 degrees)
966    let tolerance = 0.00001;   // radians
967    let max_iterations = 50;
968    
969    for iteration in 0..max_iterations {
970        let mid_angle = (low_angle + high_angle) / 2.0;
971        
972        let mut test_inputs = inputs.clone();
973        test_inputs.launch_angle = mid_angle;
974        
975        let mut solver = TrajectorySolver::new(test_inputs, wind.clone(), atmosphere.clone());
976        // Make sure we calculate far enough to reach the target
977        solver.set_max_range(target_distance * 2.0);
978        solver.set_time_step(0.001);
979        let result = solver.solve()?;
980        
981        eprintln!("  Iteration {}: angle = {:.6} rad ({:.4} deg)", 
982                 iteration, mid_angle, mid_angle * 180.0 / std::f64::consts::PI);
983        
984        // Find the height at target distance
985        let mut height_at_target = None;
986        for i in 0..result.points.len() {
987            if result.points[i].position.z >= target_distance {
988                if i > 0 {
989                    // Linear interpolation
990                    let p1 = &result.points[i - 1];
991                    let p2 = &result.points[i];
992                    let t = (target_distance - p1.position.z) / (p2.position.z - p1.position.z);
993                    height_at_target = Some(p1.position.y + t * (p2.position.y - p1.position.y));
994                } else {
995                    height_at_target = Some(result.points[i].position.y);
996                }
997                break;
998            }
999        }
1000        
1001        match height_at_target {
1002            Some(height) => {
1003                let error = height - target_height;
1004                eprintln!("    Height at target: {:.6} m, target: {:.6} m, error: {:.6} m", 
1005                         height, target_height, error);
1006                if error.abs() < 0.001 {
1007                    eprintln!("  Converged!");
1008                    return Ok(mid_angle);
1009                }
1010                
1011                if error > 0.0 {
1012                    high_angle = mid_angle;
1013                } else {
1014                    low_angle = mid_angle;
1015                }
1016            },
1017            None => {
1018                // Trajectory didn't reach target distance, increase angle
1019                low_angle = mid_angle;
1020            }
1021        }
1022        
1023        if (high_angle - low_angle).abs() < tolerance {
1024            return Ok(mid_angle);
1025        }
1026    }
1027    
1028    Err("Failed to find zero angle".into())
1029}
1030
1031// Estimate BC from trajectory data
1032pub fn estimate_bc_from_trajectory(
1033    velocity: f64,
1034    mass: f64,
1035    diameter: f64,
1036    points: &[(f64, f64)], // (distance, drop) pairs
1037) -> Result<f64, BallisticsError> {
1038    // Simple BC estimation using least squares
1039    let mut best_bc = 0.5;
1040    let mut best_error = f64::MAX;
1041    
1042    // Try different BC values
1043    for bc in (100..1000).step_by(10) {
1044        let bc_value = bc as f64 / 1000.0;
1045        
1046        let inputs = BallisticInputs {
1047            muzzle_velocity: velocity,
1048            ballistic_coefficient: bc_value,
1049            mass,
1050            diameter,
1051            ..Default::default()
1052        };
1053        
1054        let solver = TrajectorySolver::new(inputs, Default::default(), Default::default());
1055        let result = solver.solve()?;
1056        
1057        // Calculate error
1058        let mut total_error = 0.0;
1059        for (target_dist, target_drop) in points {
1060            // Find drop at this distance
1061            let mut calculated_drop = None;
1062            for i in 0..result.points.len() {
1063                if result.points[i].position.z >= *target_dist {
1064                    if i > 0 {
1065                        // Linear interpolation
1066                        let p1 = &result.points[i - 1];
1067                        let p2 = &result.points[i];
1068                        let t = (target_dist - p1.position.z) / (p2.position.z - p1.position.z);
1069                        calculated_drop = Some(-(p1.position.y + t * (p2.position.y - p1.position.y)));
1070                    } else {
1071                        calculated_drop = Some(-result.points[i].position.y);
1072                    }
1073                    break;
1074                }
1075            }
1076            
1077            if let Some(drop) = calculated_drop {
1078                let error = (drop - target_drop).abs();
1079                total_error += error * error;
1080            }
1081        }
1082        
1083        if total_error < best_error {
1084            best_error = total_error;
1085            best_bc = bc_value;
1086        }
1087    }
1088    
1089    Ok(best_bc)
1090}
1091
1092// Helper function to calculate air density
1093fn calculate_air_density(atmosphere: &AtmosphericConditions) -> f64 {
1094    // Simplified air density calculation
1095    // P / (R * T) where R is specific gas constant for dry air
1096    let r_specific = 287.058; // J/(kg·K)
1097    let temperature_k = atmosphere.temperature + 273.15;
1098    
1099    // Convert pressure from hPa to Pa
1100    let pressure_pa = atmosphere.pressure * 100.0;
1101    
1102    // Basic density calculation
1103    let density = pressure_pa / (r_specific * temperature_k);
1104    
1105    // Altitude correction (simplified)
1106    let altitude_factor = (-atmosphere.altitude / 8000.0).exp();
1107    
1108    density * altitude_factor
1109}
1110
1111// Add rand dependencies for Monte Carlo
1112use rand;
1113use rand_distr;