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 crate::cluster_bc::ClusterBCDegradation;
9use nalgebra::Vector3;
10use std::error::Error;
11use std::fmt;
12
13// Unit system for input/output
14#[derive(Debug, Clone, Copy, PartialEq)]
15pub enum UnitSystem {
16    Imperial,
17    Metric,
18}
19
20// Output format for results
21#[derive(Debug, Clone, Copy, PartialEq)]
22pub enum OutputFormat {
23    Table,
24    Json,
25    Csv,
26}
27
28// Error type for CLI operations
29#[derive(Debug)]
30pub struct BallisticsError {
31    message: String,
32}
33
34impl fmt::Display for BallisticsError {
35    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
36        write!(f, "{}", self.message)
37    }
38}
39
40impl Error for BallisticsError {}
41
42impl From<String> for BallisticsError {
43    fn from(msg: String) -> Self {
44        BallisticsError { message: msg }
45    }
46}
47
48impl From<&str> for BallisticsError {
49    fn from(msg: &str) -> Self {
50        BallisticsError { message: msg.to_string() }
51    }
52}
53
54// Ballistic input parameters - MBA-151 Reconciled Structure
55// Unified structure used by both ballistics-engine and ballistics_rust
56// Duplicates removed, all necessary fields included
57#[cfg_attr(feature = "python", pyo3::pyclass)]
58#[derive(Debug, Clone)]
59pub struct BallisticInputs {
60    // Core ballistics parameters (using intuitive names)
61    pub bc_value: f64,              // Ballistic coefficient (G1, G7, etc.)
62    pub bc_type: DragModel,         // Drag model (G1, G7, G8, etc.)
63    pub bullet_mass: f64,           // kg
64    pub muzzle_velocity: f64,       // m/s
65    pub bullet_diameter: f64,       // meters
66    pub bullet_length: f64,         // meters
67
68    // Targeting and positioning
69    pub muzzle_angle: f64,          // radians (launch angle)
70    pub target_distance: f64,       // meters
71    pub azimuth_angle: f64,         // horizontal aiming angle in radians
72    pub shooting_angle: f64,        // uphill/downhill angle in radians
73    pub sight_height: f64,          // meters above bore
74    pub muzzle_height: f64,         // meters above ground
75    pub target_height: f64,         // meters above ground for zeroing
76    pub ground_threshold: f64,      // meters below which to stop
77
78    // Environmental conditions
79    pub altitude: f64,              // meters
80    pub temperature: f64,           // Celsius
81    pub pressure: f64,              // millibars/hPa
82    pub humidity: f64,              // relative humidity (0-1)
83    pub latitude: Option<f64>,      // degrees
84
85    // Wind conditions
86    pub wind_speed: f64,            // m/s
87    pub wind_angle: f64,            // radians (0=headwind, 90=from right)
88
89    // Bullet characteristics
90    pub twist_rate: f64,            // inches per turn
91    pub is_twist_right: bool,       // right-hand twist
92    pub caliber_inches: f64,        // diameter in inches
93    pub weight_grains: f64,         // mass in grains
94    pub manufacturer: Option<String>, // Bullet manufacturer
95    pub bullet_model: Option<String>, // Bullet model name
96    pub bullet_id: Option<String>,  // Unique bullet identifier
97    pub bullet_cluster: Option<usize>, // BC cluster ID for cluster_bc module
98
99    // Integration method selection
100    pub use_rk4: bool,              // Use RK4 integration instead of Euler
101    pub use_adaptive_rk45: bool,    // Use RK45 adaptive step size integration
102
103    // Advanced effects flags
104    pub enable_advanced_effects: bool,
105    pub use_powder_sensitivity: bool,
106    pub powder_temp_sensitivity: f64,
107    pub powder_temp: f64,           // Celsius
108    pub tipoff_yaw: f64,            // radians
109    pub tipoff_decay_distance: f64, // meters
110    pub use_bc_segments: bool,
111    pub bc_segments: Option<Vec<(f64, f64)>>,  // Mach-BC pairs
112    pub bc_segments_data: Option<Vec<crate::BCSegmentData>>,  // Velocity-BC segments
113    pub use_enhanced_spin_drift: bool,
114    pub use_form_factor: bool,
115    pub enable_wind_shear: bool,
116    pub wind_shear_model: String,
117    pub enable_trajectory_sampling: bool,
118    pub sample_interval: f64,       // meters
119    pub enable_pitch_damping: bool,
120    pub enable_precession_nutation: bool,
121    pub use_cluster_bc: bool,       // Use cluster-based BC degradation
122
123    // Custom drag model support
124    pub custom_drag_table: Option<crate::drag::DragTable>,
125
126    // Legacy field for compatibility
127    pub bc_type_str: Option<String>,
128}
129
130impl Default for BallisticInputs {
131    fn default() -> Self {
132        let mass_kg = 0.01;
133        let diameter_m = 0.00762;
134        let bc = 0.5;
135        let muzzle_angle_rad = 0.0;
136        let bc_type = DragModel::G1;
137
138        Self {
139            // Core ballistics parameters
140            bc_value: bc,
141            bc_type,
142            bullet_mass: mass_kg,
143            muzzle_velocity: 800.0,
144            bullet_diameter: diameter_m,
145            bullet_length: diameter_m * 4.0,  // Approximate
146
147            // Targeting and positioning
148            muzzle_angle: muzzle_angle_rad,
149            target_distance: 100.0,
150            azimuth_angle: 0.0,
151            shooting_angle: 0.0,
152            sight_height: 0.05,
153            muzzle_height: 1.5,  // 1.5m default (standing position)
154            target_height: 0.0,  // Target at ground level by default
155            ground_threshold: -0.001,  // Stop just below ground level
156
157            // Environmental conditions
158            altitude: 0.0,
159            temperature: 15.0,
160            pressure: 1013.25,  // Standard sea level pressure (millibars)
161            humidity: 0.5,  // 50% relative humidity
162            latitude: None,
163
164            // Wind conditions
165            wind_speed: 0.0,
166            wind_angle: 0.0,
167
168            // Bullet characteristics
169            twist_rate: 12.0,  // 1:12" typical
170            is_twist_right: true,
171            caliber_inches: diameter_m / 0.0254,  // Convert to inches
172            weight_grains: mass_kg / 0.00006479891,  // Convert to grains
173            manufacturer: None,
174            bullet_model: None,
175            bullet_id: None,
176            bullet_cluster: None,
177
178            // Integration method selection
179            use_rk4: true,  // Use Runge-Kutta methods by default
180            use_adaptive_rk45: true,  // Default to RK45 adaptive for best accuracy
181
182            // Advanced effects (disabled by default)
183            enable_advanced_effects: false,
184            use_powder_sensitivity: false,
185            powder_temp_sensitivity: 0.0,
186            powder_temp: 15.0,
187            tipoff_yaw: 0.0,
188            tipoff_decay_distance: 50.0,
189            use_bc_segments: false,
190            bc_segments: None,
191            bc_segments_data: None,
192            use_enhanced_spin_drift: false,
193            use_form_factor: false,
194            enable_wind_shear: false,
195            wind_shear_model: "none".to_string(),
196            enable_trajectory_sampling: false,
197            sample_interval: 10.0,  // Default 10 meter intervals
198            enable_pitch_damping: false,
199            enable_precession_nutation: false,
200            use_cluster_bc: false,  // Disabled by default for backward compatibility
201
202            // Custom drag model support
203            custom_drag_table: None,
204
205            // Legacy field for compatibility
206            bc_type_str: None,
207        }
208    }
209}
210
211// Wind conditions
212#[derive(Debug, Clone)]
213pub struct WindConditions {
214    pub speed: f64,        // m/s
215    pub direction: f64,    // radians (0 = North, PI/2 = East)
216}
217
218impl Default for WindConditions {
219    fn default() -> Self {
220        Self {
221            speed: 0.0,
222            direction: 0.0,
223        }
224    }
225}
226
227// Atmospheric conditions
228#[derive(Debug, Clone)]
229pub struct AtmosphericConditions {
230    pub temperature: f64,  // Celsius
231    pub pressure: f64,     // hPa
232    pub humidity: f64,     // percentage (0-100)
233    pub altitude: f64,     // meters
234}
235
236impl Default for AtmosphericConditions {
237    fn default() -> Self {
238        Self {
239            temperature: 15.0,
240            pressure: 1013.25,
241            humidity: 50.0,
242            altitude: 0.0,
243        }
244    }
245}
246
247// Trajectory point data
248#[derive(Debug, Clone)]
249pub struct TrajectoryPoint {
250    pub time: f64,
251    pub position: Vector3<f64>,
252    pub velocity_magnitude: f64,
253    pub kinetic_energy: f64,
254}
255
256// Trajectory result
257#[derive(Debug, Clone)]
258pub struct TrajectoryResult {
259    pub max_range: f64,
260    pub max_height: f64,
261    pub time_of_flight: f64,
262    pub impact_velocity: f64,
263    pub impact_energy: f64,
264    pub points: Vec<TrajectoryPoint>,
265    pub sampled_points: Option<Vec<TrajectorySample>>,  // Trajectory samples at regular intervals
266    pub min_pitch_damping: Option<f64>,  // Minimum pitch damping coefficient (for stability warning)
267    pub transonic_mach: Option<f64>,      // Mach number when entering transonic regime
268    pub angular_state: Option<AngularState>,  // Final angular state if precession/nutation enabled
269    pub max_yaw_angle: Option<f64>,           // Maximum yaw angle during flight (radians)
270    pub max_precession_angle: Option<f64>,    // Maximum precession angle (radians)
271}
272
273// Trajectory solver
274pub struct TrajectorySolver {
275    inputs: BallisticInputs,
276    wind: WindConditions,
277    atmosphere: AtmosphericConditions,
278    max_range: f64,
279    time_step: f64,
280    cluster_bc: Option<ClusterBCDegradation>,
281}
282
283impl TrajectorySolver {
284    pub fn new(mut inputs: BallisticInputs, wind: WindConditions, atmosphere: AtmosphericConditions) -> Self {
285        // Ensure duplicate fields are synchronized
286        inputs.bc_type = inputs.bc_type;
287        inputs.bc_value = inputs.bc_value;
288        inputs.bullet_diameter = inputs.bullet_diameter;
289        inputs.bullet_mass = inputs.bullet_mass;
290        inputs.muzzle_angle = inputs.muzzle_angle;
291        inputs.caliber_inches = inputs.bullet_diameter / 0.0254;
292        inputs.weight_grains = inputs.bullet_mass / 0.00006479891;
293        
294        // Initialize cluster BC if enabled
295        let cluster_bc = if inputs.use_cluster_bc {
296            Some(ClusterBCDegradation::new())
297        } else {
298            None
299        };
300        
301        Self {
302            inputs,
303            wind,
304            atmosphere,
305            max_range: 1000.0,
306            time_step: 0.001,
307            cluster_bc,
308        }
309    }
310    
311    pub fn set_max_range(&mut self, range: f64) {
312        self.max_range = range;
313    }
314    
315    pub fn set_time_step(&mut self, step: f64) {
316        self.time_step = step;
317    }
318    
319    fn get_wind_at_altitude(&self, altitude_m: f64) -> Vector3<f64> {
320        // Create wind shear profile based on surface wind
321        let profile = WindShearProfile {
322            model: if self.inputs.wind_shear_model == "logarithmic" {
323                WindShearModel::Logarithmic
324            } else if self.inputs.wind_shear_model == "power" {
325                WindShearModel::PowerLaw
326            } else {
327                WindShearModel::PowerLaw  // Default to power law
328            },
329            surface_wind: WindLayer {
330                altitude_m: 0.0,
331                speed_mps: self.wind.speed,
332                direction_deg: self.wind.direction.to_degrees(),
333            },
334            reference_height: 10.0,  // Standard meteorological measurement height
335            roughness_length: 0.03,  // Short grass
336            power_exponent: 1.0 / 7.0,  // Neutral stability
337            geostrophic_wind: None,
338            custom_layers: Vec::new(),
339        };
340        
341        profile.get_wind_at_altitude(altitude_m)
342    }
343    
344    pub fn solve(&self) -> Result<TrajectoryResult, BallisticsError> {
345        if self.inputs.use_rk4 {
346            if self.inputs.use_adaptive_rk45 {
347                self.solve_rk45()
348            } else {
349                self.solve_rk4()
350            }
351        } else {
352            self.solve_euler()
353        }
354    }
355    
356    fn solve_euler(&self) -> Result<TrajectoryResult, BallisticsError> {
357        // Simple trajectory integration using Euler method
358        let mut time = 0.0;
359        let mut position = Vector3::new(0.0, self.inputs.sight_height + self.inputs.muzzle_height, 0.0);
360        // Calculate initial velocity components with both elevation and azimuth
361        let horizontal_velocity = self.inputs.muzzle_velocity * self.inputs.muzzle_angle.cos();
362        let mut velocity = Vector3::new(
363            horizontal_velocity * self.inputs.azimuth_angle.sin(),  // X: side deviation (left/right)
364            self.inputs.muzzle_velocity * self.inputs.muzzle_angle.sin(),  // Y: vertical component
365            horizontal_velocity * self.inputs.azimuth_angle.cos(),  // Z: forward component (down-range)
366        );
367        
368        let mut points = Vec::new();
369        let mut max_height = position.y;
370        let mut min_pitch_damping = 1.0;  // Track minimum pitch damping coefficient
371        let mut transonic_mach = None;    // Track when we enter transonic
372        
373        // Initialize angular state for precession/nutation tracking
374        let mut angular_state = if self.inputs.enable_precession_nutation {
375            Some(AngularState {
376                pitch_angle: 0.001,  // Small initial disturbance
377                yaw_angle: 0.001,
378                pitch_rate: 0.0,
379                yaw_rate: 0.0,
380                precession_angle: 0.0,
381                nutation_phase: 0.0,
382            })
383        } else {
384            None
385        };
386        let mut max_yaw_angle = 0.0;
387        let mut max_precession_angle = 0.0;
388        
389        // Calculate air density
390        let air_density = calculate_air_density(&self.atmosphere);
391        
392        // Wind vector
393        let wind_vector = Vector3::new(
394            self.wind.speed * self.wind.direction.sin(),
395            0.0,
396            self.wind.speed * self.wind.direction.cos(),
397        );
398        
399        // Main integration loop
400        while position.z < self.max_range && position.y >= 0.0 && time < 100.0 {
401            // Store trajectory point
402            let velocity_magnitude = velocity.magnitude();
403            let kinetic_energy = 0.5 * self.inputs.bullet_mass * velocity_magnitude * velocity_magnitude;
404            
405            points.push(TrajectoryPoint {
406                time,
407                position: position.clone(),
408                velocity_magnitude,
409                kinetic_energy,
410            });
411            
412            // Track max height
413            if position.y > max_height {
414                max_height = position.y;
415            }
416            
417            // Calculate pitch damping if enabled
418            if self.inputs.enable_pitch_damping {
419                let temp_c = self.atmosphere.temperature;
420                let temp_k = temp_c + 273.15;
421                let speed_of_sound = (1.4 * 287.05 * temp_k).sqrt();
422                let mach = velocity_magnitude / speed_of_sound;
423                
424                // Track when we enter transonic
425                if transonic_mach.is_none() && mach < 1.2 && mach > 0.8 {
426                    transonic_mach = Some(mach);
427                }
428                
429                // Calculate pitch damping coefficient
430                let bullet_type = if let Some(ref model) = self.inputs.bullet_model {
431                    model.as_str()
432                } else {
433                    "default"
434                };
435                let coeffs = PitchDampingCoefficients::from_bullet_type(bullet_type);
436                let pitch_damping = calculate_pitch_damping_coefficient(mach, &coeffs);
437                
438                // Track minimum (most critical for stability)
439                if pitch_damping < min_pitch_damping {
440                    min_pitch_damping = pitch_damping;
441                }
442            }
443            
444            // Calculate precession/nutation if enabled
445            if self.inputs.enable_precession_nutation {
446                if let Some(ref mut state) = angular_state {
447                    let velocity_magnitude = velocity.magnitude();
448                    let temp_c = self.atmosphere.temperature;
449                    let temp_k = temp_c + 273.15;
450                    let speed_of_sound = (1.4 * 287.05 * temp_k).sqrt();
451                    let mach = velocity_magnitude / speed_of_sound;
452                    
453                    // Calculate spin rate from twist rate and velocity
454                    let spin_rate_rad_s = if self.inputs.twist_rate > 0.0 {
455                        let velocity_fps = velocity_magnitude * 3.28084;
456                        let twist_rate_ft = self.inputs.twist_rate / 12.0;
457                        (velocity_fps / twist_rate_ft) * 2.0 * std::f64::consts::PI
458                    } else {
459                        0.0
460                    };
461                    
462                    // Create precession/nutation parameters
463                    let params = PrecessionNutationParams {
464                        mass_kg: self.inputs.bullet_mass,
465                        caliber_m: self.inputs.bullet_diameter,
466                        length_m: self.inputs.bullet_length,
467                        spin_rate_rad_s,
468                        spin_inertia: 6.94e-8,      // Typical value
469                        transverse_inertia: 9.13e-7, // Typical value
470                        velocity_mps: velocity_magnitude,
471                        air_density_kg_m3: air_density,
472                        mach,
473                        pitch_damping_coeff: -0.8,
474                        nutation_damping_factor: 0.05,
475                    };
476                    
477                    // Update angular state
478                    *state = calculate_combined_angular_motion(
479                        &params,
480                        state,
481                        time,
482                        self.time_step,
483                        0.001,  // Initial disturbance
484                    );
485                    
486                    // Track maximums
487                    if state.yaw_angle.abs() > max_yaw_angle {
488                        max_yaw_angle = state.yaw_angle.abs();
489                    }
490                    if state.precession_angle.abs() > max_precession_angle {
491                        max_precession_angle = state.precession_angle.abs();
492                    }
493                }
494            }
495            
496            // Calculate drag with altitude-dependent wind if enabled
497            let actual_wind = if self.inputs.enable_wind_shear {
498                self.get_wind_at_altitude(position.y)
499            } else {
500                wind_vector.clone()
501            };
502            let velocity_rel = velocity - actual_wind;
503            let velocity_rel_mag = velocity_rel.magnitude();
504            let drag_coefficient = self.calculate_drag_coefficient(velocity_rel_mag);
505            
506            // Calculate drag force
507            let drag_force = 0.5 * air_density * drag_coefficient * 
508                            self.inputs.bullet_diameter * self.inputs.bullet_diameter * 
509                            std::f64::consts::PI / 4.0 * velocity_rel_mag * velocity_rel_mag;
510            
511            // Calculate acceleration
512            let drag_acceleration = -drag_force / self.inputs.bullet_mass;
513            let acceleration = Vector3::new(
514                drag_acceleration * velocity_rel.x / velocity_rel_mag,
515                drag_acceleration * velocity_rel.y / velocity_rel_mag - 9.80665,
516                drag_acceleration * velocity_rel.z / velocity_rel_mag,
517            );
518            
519            // Update state
520            velocity += acceleration * self.time_step;
521            position += velocity * self.time_step;
522            time += self.time_step;
523        }
524        
525        // Get final values
526        let last_point = points.last().ok_or("No trajectory points generated")?;
527        
528        // Create trajectory sampling data if enabled
529        let sampled_points = if self.inputs.enable_trajectory_sampling {
530            let trajectory_data = TrajectoryData {
531                times: points.iter().map(|p| p.time).collect(),
532                positions: points.iter().map(|p| p.position.clone()).collect(),
533                velocities: points.iter().map(|p| {
534                    // Reconstruct velocity vectors from magnitude (approximate)
535                    Vector3::new(0.0, 0.0, p.velocity_magnitude)
536                }).collect(),
537                transonic_distances: Vec::new(),  // TODO: Track Mach transitions
538            };
539            
540            let outputs = TrajectoryOutputs {
541                target_distance_horiz_m: last_point.position.z,
542                target_vertical_height_m: last_point.position.y,
543                time_of_flight_s: last_point.time,
544                max_ord_dist_horiz_m: max_height,
545            };
546            
547            // Sample at specified intervals
548            let samples = sample_trajectory(&trajectory_data, &outputs, self.inputs.sample_interval, self.inputs.bullet_mass);
549            Some(samples)
550        } else {
551            None
552        };
553        
554        Ok(TrajectoryResult {
555            max_range: last_point.position.z,
556            max_height,
557            time_of_flight: last_point.time,
558            impact_velocity: last_point.velocity_magnitude,
559            impact_energy: last_point.kinetic_energy,
560            points,
561            sampled_points,
562            min_pitch_damping: if self.inputs.enable_pitch_damping { Some(min_pitch_damping) } else { None },
563            transonic_mach: transonic_mach,
564            angular_state: angular_state,
565            max_yaw_angle: if self.inputs.enable_precession_nutation { Some(max_yaw_angle) } else { None },
566            max_precession_angle: if self.inputs.enable_precession_nutation { Some(max_precession_angle) } else { None },
567        })
568    }
569    
570    fn solve_rk4(&self) -> Result<TrajectoryResult, BallisticsError> {
571        // RK4 trajectory integration for better accuracy
572        let mut time = 0.0;
573        let mut position = Vector3::new(0.0, self.inputs.sight_height + self.inputs.muzzle_height, 0.0);
574        
575        // Calculate initial velocity components with both elevation and azimuth
576        let horizontal_velocity = self.inputs.muzzle_velocity * self.inputs.muzzle_angle.cos();
577        let mut velocity = Vector3::new(
578            horizontal_velocity * self.inputs.azimuth_angle.sin(),
579            self.inputs.muzzle_velocity * self.inputs.muzzle_angle.sin(),
580            horizontal_velocity * self.inputs.azimuth_angle.cos(),
581        );
582        
583        let mut points = Vec::new();
584        let mut max_height = position.y;
585        let mut min_pitch_damping = 1.0;  // Track minimum pitch damping coefficient
586        let mut transonic_mach = None;    // Track when we enter transonic
587        
588        // Initialize angular state for precession/nutation tracking
589        let mut angular_state = if self.inputs.enable_precession_nutation {
590            Some(AngularState {
591                pitch_angle: 0.001,  // Small initial disturbance
592                yaw_angle: 0.001,
593                pitch_rate: 0.0,
594                yaw_rate: 0.0,
595                precession_angle: 0.0,
596                nutation_phase: 0.0,
597            })
598        } else {
599            None
600        };
601        let mut max_yaw_angle = 0.0;
602        let mut max_precession_angle = 0.0;
603        
604        // Calculate air density
605        let air_density = calculate_air_density(&self.atmosphere);
606        
607        // Wind vector
608        let wind_vector = Vector3::new(
609            self.wind.speed * self.wind.direction.sin(),
610            0.0,
611            self.wind.speed * self.wind.direction.cos(),
612        );
613        
614        // Main RK4 integration loop
615        while position.z < self.max_range && position.y >= 0.0 && time < 100.0 {
616            // Store trajectory point
617            let velocity_magnitude = velocity.magnitude();
618            let kinetic_energy = 0.5 * self.inputs.bullet_mass * velocity_magnitude * velocity_magnitude;
619            
620            points.push(TrajectoryPoint {
621                time,
622                position: position.clone(),
623                velocity_magnitude,
624                kinetic_energy,
625            });
626            
627            if position.y > max_height {
628                max_height = position.y;
629            }
630            
631            // Calculate pitch damping if enabled (RK4 solver)
632            if self.inputs.enable_pitch_damping {
633                let temp_c = self.atmosphere.temperature;
634                let temp_k = temp_c + 273.15;
635                let speed_of_sound = (1.4 * 287.05 * temp_k).sqrt();
636                let mach = velocity_magnitude / speed_of_sound;
637                
638                // Track when we enter transonic
639                if transonic_mach.is_none() && mach < 1.2 && mach > 0.8 {
640                    transonic_mach = Some(mach);
641                }
642                
643                // Calculate pitch damping coefficient
644                let bullet_type = if let Some(ref model) = self.inputs.bullet_model {
645                    model.as_str()
646                } else {
647                    "default"
648                };
649                let coeffs = PitchDampingCoefficients::from_bullet_type(bullet_type);
650                let pitch_damping = calculate_pitch_damping_coefficient(mach, &coeffs);
651                
652                // Track minimum (most critical for stability)
653                if pitch_damping < min_pitch_damping {
654                    min_pitch_damping = pitch_damping;
655                }
656            }
657            
658            // Calculate precession/nutation if enabled (RK4 solver)
659            if self.inputs.enable_precession_nutation {
660                if let Some(ref mut state) = angular_state {
661                    let velocity_magnitude = velocity.magnitude();
662                    let temp_c = self.atmosphere.temperature;
663                    let temp_k = temp_c + 273.15;
664                    let speed_of_sound = (1.4 * 287.05 * temp_k).sqrt();
665                    let mach = velocity_magnitude / speed_of_sound;
666                    
667                    // Calculate spin rate from twist rate and velocity
668                    let spin_rate_rad_s = if self.inputs.twist_rate > 0.0 {
669                        let velocity_fps = velocity_magnitude * 3.28084;
670                        let twist_rate_ft = self.inputs.twist_rate / 12.0;
671                        (velocity_fps / twist_rate_ft) * 2.0 * std::f64::consts::PI
672                    } else {
673                        0.0
674                    };
675                    
676                    // Create precession/nutation parameters
677                    let params = PrecessionNutationParams {
678                        mass_kg: self.inputs.bullet_mass,
679                        caliber_m: self.inputs.bullet_diameter,
680                        length_m: self.inputs.bullet_length,
681                        spin_rate_rad_s,
682                        spin_inertia: 6.94e-8,      // Typical value
683                        transverse_inertia: 9.13e-7, // Typical value
684                        velocity_mps: velocity_magnitude,
685                        air_density_kg_m3: air_density,
686                        mach,
687                        pitch_damping_coeff: -0.8,
688                        nutation_damping_factor: 0.05,
689                    };
690                    
691                    // Update angular state
692                    *state = calculate_combined_angular_motion(
693                        &params,
694                        state,
695                        time,
696                        self.time_step,
697                        0.001,  // Initial disturbance
698                    );
699                    
700                    // Track maximums
701                    if state.yaw_angle.abs() > max_yaw_angle {
702                        max_yaw_angle = state.yaw_angle.abs();
703                    }
704                    if state.precession_angle.abs() > max_precession_angle {
705                        max_precession_angle = state.precession_angle.abs();
706                    }
707                }
708            }
709            
710            // RK4 method
711            let dt = self.time_step;
712            
713            // k1
714            let acc1 = self.calculate_acceleration(&position, &velocity, air_density, &wind_vector);
715            
716            // k2
717            let pos2 = position + velocity * (dt * 0.5);
718            let vel2 = velocity + acc1 * (dt * 0.5);
719            let acc2 = self.calculate_acceleration(&pos2, &vel2, air_density, &wind_vector);
720            
721            // k3
722            let pos3 = position + vel2 * (dt * 0.5);
723            let vel3 = velocity + acc2 * (dt * 0.5);
724            let acc3 = self.calculate_acceleration(&pos3, &vel3, air_density, &wind_vector);
725            
726            // k4
727            let pos4 = position + vel3 * dt;
728            let vel4 = velocity + acc3 * dt;
729            let acc4 = self.calculate_acceleration(&pos4, &vel4, air_density, &wind_vector);
730            
731            // Update position and velocity
732            position += (velocity + vel2 * 2.0 + vel3 * 2.0 + vel4) * (dt / 6.0);
733            velocity += (acc1 + acc2 * 2.0 + acc3 * 2.0 + acc4) * (dt / 6.0);
734            time += dt;
735        }
736        
737        // Get final values
738        let last_point = points.last().ok_or("No trajectory points generated")?;
739        
740        // Create trajectory sampling data if enabled
741        let sampled_points = if self.inputs.enable_trajectory_sampling {
742            let trajectory_data = TrajectoryData {
743                times: points.iter().map(|p| p.time).collect(),
744                positions: points.iter().map(|p| p.position.clone()).collect(),
745                velocities: points.iter().map(|p| {
746                    // Reconstruct velocity vectors from magnitude (approximate)
747                    Vector3::new(0.0, 0.0, p.velocity_magnitude)
748                }).collect(),
749                transonic_distances: Vec::new(),  // TODO: Track Mach transitions
750            };
751            
752            let outputs = TrajectoryOutputs {
753                target_distance_horiz_m: last_point.position.z,
754                target_vertical_height_m: last_point.position.y,
755                time_of_flight_s: last_point.time,
756                max_ord_dist_horiz_m: max_height,
757            };
758            
759            // Sample at specified intervals
760            let samples = sample_trajectory(&trajectory_data, &outputs, self.inputs.sample_interval, self.inputs.bullet_mass);
761            Some(samples)
762        } else {
763            None
764        };
765        
766        Ok(TrajectoryResult {
767            max_range: last_point.position.z,
768            max_height,
769            time_of_flight: last_point.time,
770            impact_velocity: last_point.velocity_magnitude,
771            impact_energy: last_point.kinetic_energy,
772            points,
773            sampled_points,
774            min_pitch_damping: if self.inputs.enable_pitch_damping { Some(min_pitch_damping) } else { None },
775            transonic_mach: transonic_mach,
776            angular_state: angular_state,
777            max_yaw_angle: if self.inputs.enable_precession_nutation { Some(max_yaw_angle) } else { None },
778            max_precession_angle: if self.inputs.enable_precession_nutation { Some(max_precession_angle) } else { None },
779        })
780    }
781    
782    fn solve_rk45(&self) -> Result<TrajectoryResult, BallisticsError> {
783        // RK45 adaptive step size integration (Dormand-Prince method)
784        let mut time = 0.0;
785        let mut position = Vector3::new(0.0, self.inputs.sight_height + self.inputs.muzzle_height, 0.0);
786        
787        // Calculate initial velocity components
788        let horizontal_velocity = self.inputs.muzzle_velocity * self.inputs.muzzle_angle.cos();
789        let mut velocity = Vector3::new(
790            horizontal_velocity * self.inputs.azimuth_angle.sin(),
791            self.inputs.muzzle_velocity * self.inputs.muzzle_angle.sin(),
792            horizontal_velocity * self.inputs.azimuth_angle.cos(),
793        );
794        
795        let mut points = Vec::new();
796        let mut max_height = position.y;
797        let mut dt = 0.001;  // Initial step size
798        let tolerance = 1e-6;  // Error tolerance
799        let safety_factor = 0.9;  // Safety factor for step size adjustment
800        let max_dt = 0.01;  // Maximum step size
801        let min_dt = 1e-6;   // Minimum step size
802        
803        // Add a point counter to debug
804        let mut iteration_count = 0;
805        const MAX_ITERATIONS: usize = 100000;
806        
807        while position.z < self.max_range && position.y > self.inputs.ground_threshold && time < 100.0 {
808            iteration_count += 1;
809            if iteration_count > MAX_ITERATIONS {
810                break; // Prevent infinite loop
811            }
812            
813            // Store current point
814            let velocity_magnitude = velocity.magnitude();
815            let kinetic_energy = 0.5 * self.inputs.bullet_mass * velocity_magnitude.powi(2);
816            
817            points.push(TrajectoryPoint {
818                time,
819                position: position.clone(),
820                velocity_magnitude,
821                kinetic_energy,
822            });
823            
824            if position.y > max_height {
825                max_height = position.y;
826            }
827            
828            // Get atmospheric conditions and wind
829            let air_density = calculate_air_density(&self.atmosphere);
830            let wind_vector = Vector3::new(
831                -self.wind.speed * self.wind.direction.sin(),
832                0.0,
833                -self.wind.speed * self.wind.direction.cos(),
834            );
835            
836            // RK45 step with adaptive step size
837            let (new_pos, new_vel, new_dt) = self.rk45_step(
838                &position,
839                &velocity,
840                dt,
841                air_density,
842                &wind_vector,
843                tolerance,
844            );
845            
846            // Update step size with safety factor and bounds
847            dt = (safety_factor * new_dt).clamp(min_dt, max_dt);
848            
849            // Update state
850            position = new_pos;
851            velocity = new_vel;
852            time += dt;
853        }
854        
855        // Ensure we have at least one point
856        if points.is_empty() {
857            return Err(BallisticsError::from("No trajectory points calculated"));
858        }
859        
860        let last_point = points.last().unwrap();
861        
862        Ok(TrajectoryResult {
863            max_range: last_point.position.z,
864            max_height,
865            time_of_flight: last_point.time,
866            impact_velocity: last_point.velocity_magnitude,
867            impact_energy: last_point.kinetic_energy,
868            points,
869            sampled_points: None,  // Simplified - no trajectory sampling in RK45 for now
870            min_pitch_damping: None,
871            transonic_mach: None,
872            angular_state: None,
873            max_yaw_angle: None,
874            max_precession_angle: None,
875        })
876    }
877    
878    fn rk45_step(
879        &self,
880        position: &Vector3<f64>,
881        velocity: &Vector3<f64>,
882        dt: f64,
883        air_density: f64,
884        wind_vector: &Vector3<f64>,
885        tolerance: f64,
886    ) -> (Vector3<f64>, Vector3<f64>, f64) {
887        // Dormand-Prince coefficients
888        const A21: f64 = 1.0 / 5.0;
889        const A31: f64 = 3.0 / 40.0;
890        const A32: f64 = 9.0 / 40.0;
891        const A41: f64 = 44.0 / 45.0;
892        const A42: f64 = -56.0 / 15.0;
893        const A43: f64 = 32.0 / 9.0;
894        const A51: f64 = 19372.0 / 6561.0;
895        const A52: f64 = -25360.0 / 2187.0;
896        const A53: f64 = 64448.0 / 6561.0;
897        const A54: f64 = -212.0 / 729.0;
898        const A61: f64 = 9017.0 / 3168.0;
899        const A62: f64 = -355.0 / 33.0;
900        const A63: f64 = 46732.0 / 5247.0;
901        const A64: f64 = 49.0 / 176.0;
902        const A65: f64 = -5103.0 / 18656.0;
903        const A71: f64 = 35.0 / 384.0;
904        const A73: f64 = 500.0 / 1113.0;
905        const A74: f64 = 125.0 / 192.0;
906        const A75: f64 = -2187.0 / 6784.0;
907        const A76: f64 = 11.0 / 84.0;
908        
909        // 5th order coefficients
910        const B1: f64 = 35.0 / 384.0;
911        const B3: f64 = 500.0 / 1113.0;
912        const B4: f64 = 125.0 / 192.0;
913        const B5: f64 = -2187.0 / 6784.0;
914        const B6: f64 = 11.0 / 84.0;
915        
916        // 4th order coefficients for error estimation
917        const B1_ERR: f64 = 5179.0 / 57600.0;
918        const B3_ERR: f64 = 7571.0 / 16695.0;
919        const B4_ERR: f64 = 393.0 / 640.0;
920        const B5_ERR: f64 = -92097.0 / 339200.0;
921        const B6_ERR: f64 = 187.0 / 2100.0;
922        const B7_ERR: f64 = 1.0 / 40.0;
923        
924        // Compute RK45 stages
925        let k1_v = self.calculate_acceleration(position, velocity, air_density, wind_vector);
926        let k1_p = velocity.clone();
927        
928        let p2 = position + dt * A21 * k1_p;
929        let v2 = velocity + dt * A21 * k1_v;
930        let k2_v = self.calculate_acceleration(&p2, &v2, air_density, wind_vector);
931        let k2_p = v2;
932        
933        let p3 = position + dt * (A31 * k1_p + A32 * k2_p);
934        let v3 = velocity + dt * (A31 * k1_v + A32 * k2_v);
935        let k3_v = self.calculate_acceleration(&p3, &v3, air_density, wind_vector);
936        let k3_p = v3;
937        
938        let p4 = position + dt * (A41 * k1_p + A42 * k2_p + A43 * k3_p);
939        let v4 = velocity + dt * (A41 * k1_v + A42 * k2_v + A43 * k3_v);
940        let k4_v = self.calculate_acceleration(&p4, &v4, air_density, wind_vector);
941        let k4_p = v4;
942        
943        let p5 = position + dt * (A51 * k1_p + A52 * k2_p + A53 * k3_p + A54 * k4_p);
944        let v5 = velocity + dt * (A51 * k1_v + A52 * k2_v + A53 * k3_v + A54 * k4_v);
945        let k5_v = self.calculate_acceleration(&p5, &v5, air_density, wind_vector);
946        let k5_p = v5;
947        
948        let p6 = position + dt * (A61 * k1_p + A62 * k2_p + A63 * k3_p + A64 * k4_p + A65 * k5_p);
949        let v6 = velocity + dt * (A61 * k1_v + A62 * k2_v + A63 * k3_v + A64 * k4_v + A65 * k5_v);
950        let k6_v = self.calculate_acceleration(&p6, &v6, air_density, wind_vector);
951        let k6_p = v6;
952        
953        let p7 = position + dt * (A71 * k1_p + A73 * k3_p + A74 * k4_p + A75 * k5_p + A76 * k6_p);
954        let v7 = velocity + dt * (A71 * k1_v + A73 * k3_v + A74 * k4_v + A75 * k5_v + A76 * k6_v);
955        let k7_v = self.calculate_acceleration(&p7, &v7, air_density, wind_vector);
956        let k7_p = v7;
957        
958        // 5th order solution
959        let new_pos = position + dt * (B1 * k1_p + B3 * k3_p + B4 * k4_p + B5 * k5_p + B6 * k6_p);
960        let new_vel = velocity + dt * (B1 * k1_v + B3 * k3_v + B4 * k4_v + B5 * k5_v + B6 * k6_v);
961        
962        // 4th order solution for error estimate
963        let pos_err = position + dt * (B1_ERR * k1_p + B3_ERR * k3_p + B4_ERR * k4_p + B5_ERR * k5_p + B6_ERR * k6_p + B7_ERR * k7_p);
964        let vel_err = velocity + dt * (B1_ERR * k1_v + B3_ERR * k3_v + B4_ERR * k4_v + B5_ERR * k5_v + B6_ERR * k6_v + B7_ERR * k7_v);
965        
966        // Estimate error
967        let pos_error = (new_pos - pos_err).magnitude();
968        let vel_error = (new_vel - vel_err).magnitude();
969        let error = (pos_error + vel_error) / (1.0 + position.magnitude() + velocity.magnitude());
970        
971        // Calculate new step size
972        let dt_new = if error < tolerance {
973            dt * (tolerance / error).powf(0.2).min(2.0)
974        } else {
975            dt * (tolerance / error).powf(0.25).max(0.1)
976        };
977        
978        (new_pos, new_vel, dt_new)
979    }
980    
981    fn calculate_acceleration(&self, position: &Vector3<f64>, velocity: &Vector3<f64>, air_density: f64, wind_vector: &Vector3<f64>) -> Vector3<f64> {
982        // Calculate altitude-dependent wind if wind shear is enabled
983        let actual_wind = if self.inputs.enable_wind_shear {
984            self.get_wind_at_altitude(position.y)
985        } else {
986            wind_vector.clone()
987        };
988        
989        let relative_velocity = velocity - &actual_wind;
990        let velocity_magnitude = relative_velocity.magnitude();
991        
992        if velocity_magnitude < 0.001 {
993            return Vector3::new(0.0, -9.81, 0.0);
994        }
995        
996        // Calculate drag force
997        let cd = self.calculate_drag_coefficient(velocity_magnitude);
998        let reference_area = std::f64::consts::PI * (self.inputs.bullet_diameter / 2.0).powi(2);
999        
1000        // Apply cluster BC correction if enabled
1001        let effective_bc = if let Some(ref cluster_bc) = self.cluster_bc {
1002            // Convert velocity to fps for cluster BC calculation
1003            let velocity_fps = velocity_magnitude * 3.28084;
1004            cluster_bc.apply_correction(
1005                self.inputs.bc_value,
1006                self.inputs.caliber_inches * 0.0254,  // Convert back to meters for consistency
1007                self.inputs.weight_grains,
1008                velocity_fps
1009            )
1010        } else {
1011            self.inputs.bc_value
1012        };
1013        
1014        let drag_magnitude = 0.5 * air_density * velocity_magnitude.powi(2) * cd * reference_area / effective_bc;
1015        
1016        // Drag acts opposite to velocity
1017        let drag_force = -relative_velocity.normalize() * drag_magnitude;
1018        
1019        // Total acceleration = drag/mass + gravity
1020        let acceleration = drag_force / self.inputs.bullet_mass + Vector3::new(0.0, -9.81, 0.0);
1021        
1022        acceleration
1023    }
1024    
1025    fn calculate_drag_coefficient(&self, velocity: f64) -> f64 {
1026        // Calculate speed of sound based on atmospheric temperature
1027        // Use standard atmosphere temperature at sea level if not available
1028        let temp_c = self.atmosphere.temperature;
1029        let temp_k = temp_c + 273.15;
1030        let gamma = 1.4;  // Ratio of specific heats for air
1031        let r_specific = 287.05;  // Specific gas constant for air (J/kg·K)
1032        let speed_of_sound = (gamma * r_specific * temp_k).sqrt();
1033        let mach = velocity / speed_of_sound;
1034        
1035        // Base drag coefficient from drag model
1036        let base_cd = match self.inputs.bc_type {
1037            DragModel::G1 => 0.5,
1038            DragModel::G7 => 0.4,
1039            _ => 0.45,
1040        };
1041        
1042        // Determine projectile shape for transonic corrections
1043        let projectile_shape = if let Some(ref model) = self.inputs.bullet_model {
1044            // Try to determine shape from bullet model string
1045            if model.to_lowercase().contains("boat") || model.to_lowercase().contains("bt") {
1046                ProjectileShape::BoatTail
1047            } else if model.to_lowercase().contains("round") || model.to_lowercase().contains("rn") {
1048                ProjectileShape::RoundNose
1049            } else if model.to_lowercase().contains("flat") || model.to_lowercase().contains("fb") {
1050                ProjectileShape::FlatBase
1051            } else {
1052                // Use heuristic based on caliber, weight, and drag model
1053                get_projectile_shape(
1054                    self.inputs.bullet_diameter,
1055                    self.inputs.bullet_mass / 0.00006479891,  // Convert kg to grains
1056                    &self.inputs.bc_type.to_string()
1057                )
1058            }
1059        } else {
1060            // Use heuristic based on caliber, weight, and drag model
1061            get_projectile_shape(
1062                self.inputs.bullet_diameter,
1063                self.inputs.bullet_mass / 0.00006479891,  // Convert kg to grains
1064                &self.inputs.bc_type.to_string()
1065            )
1066        };
1067        
1068        // Apply transonic corrections
1069        // Enable wave drag if advanced effects are enabled
1070        let include_wave_drag = self.inputs.enable_advanced_effects;
1071        transonic_correction(mach, base_cd, projectile_shape, include_wave_drag)
1072    }
1073}
1074
1075// Monte Carlo parameters
1076#[derive(Debug, Clone)]
1077pub struct MonteCarloParams {
1078    pub num_simulations: usize,
1079    pub velocity_std_dev: f64,
1080    pub angle_std_dev: f64,
1081    pub bc_std_dev: f64,
1082    pub wind_speed_std_dev: f64,
1083    pub target_distance: Option<f64>,
1084    pub base_wind_speed: f64,
1085    pub base_wind_direction: f64,
1086    pub azimuth_std_dev: f64,  // Horizontal aiming variation in radians
1087}
1088
1089impl Default for MonteCarloParams {
1090    fn default() -> Self {
1091        Self {
1092            num_simulations: 1000,
1093            velocity_std_dev: 1.0,
1094            angle_std_dev: 0.001,
1095            bc_std_dev: 0.01,
1096            wind_speed_std_dev: 1.0,
1097            target_distance: None,
1098            base_wind_speed: 0.0,
1099            base_wind_direction: 0.0,
1100            azimuth_std_dev: 0.001,  // Default horizontal spread ~0.057 degrees
1101        }
1102    }
1103}
1104
1105// Monte Carlo results
1106#[derive(Debug, Clone)]
1107pub struct MonteCarloResults {
1108    pub ranges: Vec<f64>,
1109    pub impact_velocities: Vec<f64>,
1110    pub impact_positions: Vec<Vector3<f64>>,
1111}
1112
1113// Run Monte Carlo simulation (backwards compatibility)
1114pub fn run_monte_carlo(
1115    base_inputs: BallisticInputs,
1116    params: MonteCarloParams,
1117) -> Result<MonteCarloResults, BallisticsError> {
1118    let base_wind = WindConditions {
1119        speed: params.base_wind_speed,
1120        direction: params.base_wind_direction,
1121    };
1122    run_monte_carlo_with_wind(base_inputs, base_wind, params)
1123}
1124
1125// Run Monte Carlo simulation with wind
1126pub fn run_monte_carlo_with_wind(
1127    base_inputs: BallisticInputs,
1128    base_wind: WindConditions,
1129    params: MonteCarloParams,
1130) -> Result<MonteCarloResults, BallisticsError> {
1131    use rand::thread_rng;
1132    use rand_distr::{Distribution, Normal};
1133    
1134    let mut rng = thread_rng();
1135    let mut ranges = Vec::new();
1136    let mut impact_velocities = Vec::new();
1137    let mut impact_positions = Vec::new();
1138    
1139    // First, calculate baseline trajectory with no variations
1140    let baseline_solver = TrajectorySolver::new(base_inputs.clone(), base_wind.clone(), Default::default());
1141    let baseline_result = baseline_solver.solve()?;
1142    let baseline_impact = baseline_result.points.last()
1143        .ok_or("No baseline trajectory points")?
1144        .position.clone();
1145    
1146    // Create normal distributions for variations
1147    let velocity_dist = Normal::new(base_inputs.muzzle_velocity, params.velocity_std_dev)
1148        .map_err(|e| format!("Invalid velocity distribution: {}", e))?;
1149    let angle_dist = Normal::new(base_inputs.muzzle_angle, params.angle_std_dev)
1150        .map_err(|e| format!("Invalid angle distribution: {}", e))?;
1151    let bc_dist = Normal::new(base_inputs.bc_value, params.bc_std_dev)
1152        .map_err(|e| format!("Invalid BC distribution: {}", e))?;
1153    let wind_speed_dist = Normal::new(base_wind.speed, params.wind_speed_std_dev)
1154        .map_err(|e| format!("Invalid wind speed distribution: {}", e))?;
1155    let wind_dir_dist = Normal::new(base_wind.direction, params.wind_speed_std_dev * 0.1)  // Small variation in direction
1156        .map_err(|e| format!("Invalid wind direction distribution: {}", e))?;
1157    let azimuth_dist = Normal::new(base_inputs.azimuth_angle, params.azimuth_std_dev)
1158        .map_err(|e| format!("Invalid azimuth distribution: {}", e))?;
1159    
1160    // Create distribution for pointing errors (simulates shooter's aiming consistency)
1161    let distance = baseline_impact.z;  // Distance to target
1162    let pointing_error_dist = Normal::new(0.0, params.angle_std_dev * distance)
1163        .map_err(|e| format!("Invalid pointing distribution: {}", e))?;
1164    
1165    for _ in 0..params.num_simulations {
1166        // Create varied inputs
1167        let mut inputs = base_inputs.clone();
1168        inputs.muzzle_velocity = velocity_dist.sample(&mut rng).max(0.0);
1169        inputs.muzzle_angle = angle_dist.sample(&mut rng);
1170        inputs.bc_value = bc_dist.sample(&mut rng).max(0.01);
1171        inputs.azimuth_angle = azimuth_dist.sample(&mut rng);  // Add horizontal variation
1172        
1173        // Create varied wind (now based on base wind conditions)
1174        let wind = WindConditions {
1175            speed: wind_speed_dist.sample(&mut rng).abs(),
1176            direction: wind_dir_dist.sample(&mut rng),
1177        };
1178        
1179        // Run trajectory
1180        let solver = TrajectorySolver::new(inputs, wind, Default::default());
1181        match solver.solve() {
1182            Ok(result) => {
1183                ranges.push(result.max_range);
1184                impact_velocities.push(result.impact_velocity);
1185                if let Some(last_point) = result.points.last() {
1186                    // Calculate physical deviation from baseline impact point
1187                    let mut deviation = Vector3::new(
1188                        last_point.position.x - baseline_impact.x,  // Lateral deviation
1189                        last_point.position.y - baseline_impact.y,  // Vertical deviation
1190                        last_point.position.z - baseline_impact.z,  // Range deviation
1191                    );
1192                    
1193                    // Add additional pointing error to simulate realistic group sizes
1194                    // This represents the shooter's ability to aim consistently
1195                    let pointing_error_y = pointing_error_dist.sample(&mut rng);
1196                    deviation.y += pointing_error_y;
1197                    
1198                    impact_positions.push(deviation);
1199                }
1200            },
1201            Err(_) => {
1202                // Skip failed simulations
1203                continue;
1204            }
1205        }
1206    }
1207    
1208    if ranges.is_empty() {
1209        return Err("No successful simulations".into());
1210    }
1211    
1212    Ok(MonteCarloResults {
1213        ranges,
1214        impact_velocities,
1215        impact_positions,
1216    })
1217}
1218
1219// Calculate zero angle for a target
1220pub fn calculate_zero_angle(
1221    inputs: BallisticInputs,
1222    target_distance: f64,
1223    target_height: f64,
1224) -> Result<f64, BallisticsError> {
1225    calculate_zero_angle_with_conditions(
1226        inputs,
1227        target_distance,
1228        target_height,
1229        WindConditions::default(),
1230        AtmosphericConditions::default(),
1231    )
1232}
1233
1234pub fn calculate_zero_angle_with_conditions(
1235    inputs: BallisticInputs,
1236    target_distance: f64,
1237    target_height: f64,
1238    wind: WindConditions,
1239    atmosphere: AtmosphericConditions,
1240) -> Result<f64, BallisticsError> {
1241    // Binary search for the angle that hits the target
1242    let mut low_angle = -0.2; // radians (about -11 degrees)
1243    let mut high_angle = 0.2;  // radians (about 11 degrees)
1244    let tolerance = 0.00001;   // radians
1245    let max_iterations = 50;
1246    
1247    for iteration in 0..max_iterations {
1248        let mid_angle = (low_angle + high_angle) / 2.0;
1249        
1250        let mut test_inputs = inputs.clone();
1251        test_inputs.muzzle_angle = mid_angle;
1252        
1253        let mut solver = TrajectorySolver::new(test_inputs, wind.clone(), atmosphere.clone());
1254        // Make sure we calculate far enough to reach the target
1255        solver.set_max_range(target_distance * 2.0);
1256        solver.set_time_step(0.001);
1257        let result = solver.solve()?;
1258        
1259        eprintln!("  Iteration {}: angle = {:.6} rad ({:.4} deg)", 
1260                 iteration, mid_angle, mid_angle * 180.0 / std::f64::consts::PI);
1261        
1262        // Find the height at target distance
1263        let mut height_at_target = None;
1264        for i in 0..result.points.len() {
1265            if result.points[i].position.z >= target_distance {
1266                if i > 0 {
1267                    // Linear interpolation
1268                    let p1 = &result.points[i - 1];
1269                    let p2 = &result.points[i];
1270                    let t = (target_distance - p1.position.z) / (p2.position.z - p1.position.z);
1271                    height_at_target = Some(p1.position.y + t * (p2.position.y - p1.position.y));
1272                } else {
1273                    height_at_target = Some(result.points[i].position.y);
1274                }
1275                break;
1276            }
1277        }
1278        
1279        match height_at_target {
1280            Some(height) => {
1281                let error = height - target_height;
1282                eprintln!("    Height at target: {:.6} m, target: {:.6} m, error: {:.6} m", 
1283                         height, target_height, error);
1284                if error.abs() < 0.001 {
1285                    eprintln!("  Converged!");
1286                    return Ok(mid_angle);
1287                }
1288                
1289                if error > 0.0 {
1290                    high_angle = mid_angle;
1291                } else {
1292                    low_angle = mid_angle;
1293                }
1294            },
1295            None => {
1296                // Trajectory didn't reach target distance, increase angle
1297                low_angle = mid_angle;
1298            }
1299        }
1300        
1301        if (high_angle - low_angle).abs() < tolerance {
1302            return Ok(mid_angle);
1303        }
1304    }
1305    
1306    Err("Failed to find zero angle".into())
1307}
1308
1309// Estimate BC from trajectory data
1310pub fn estimate_bc_from_trajectory(
1311    velocity: f64,
1312    mass: f64,
1313    diameter: f64,
1314    points: &[(f64, f64)], // (distance, drop) pairs
1315) -> Result<f64, BallisticsError> {
1316    // Simple BC estimation using least squares
1317    let mut best_bc = 0.5;
1318    let mut best_error = f64::MAX;
1319    let mut found_valid = false;
1320    
1321    // Try different BC values
1322    for bc in (100..1000).step_by(10) {
1323        let bc_value = bc as f64 / 1000.0;
1324        
1325        let inputs = BallisticInputs {
1326            muzzle_velocity: velocity,
1327            bc_value,
1328            bullet_mass: mass,
1329            bullet_diameter: diameter,
1330            ..Default::default()
1331        };
1332        
1333        let mut solver = TrajectorySolver::new(inputs, Default::default(), Default::default());
1334        // Set max range for BC estimation
1335        solver.set_max_range(points.last().map(|(d, _)| *d * 1.5).unwrap_or(1000.0));
1336        
1337        let result = match solver.solve() {
1338            Ok(r) => r,
1339            Err(_) => continue, // Skip this BC value if solve fails
1340        };
1341        
1342        // Calculate error
1343        let mut total_error = 0.0;
1344        for (target_dist, target_drop) in points {
1345            // Find drop at this distance
1346            let mut calculated_drop = None;
1347            for i in 0..result.points.len() {
1348                if result.points[i].position.z >= *target_dist {
1349                    if i > 0 {
1350                        // Linear interpolation
1351                        let p1 = &result.points[i - 1];
1352                        let p2 = &result.points[i];
1353                        let t = (target_dist - p1.position.z) / (p2.position.z - p1.position.z);
1354                        calculated_drop = Some(-(p1.position.y + t * (p2.position.y - p1.position.y)));
1355                    } else {
1356                        calculated_drop = Some(-result.points[i].position.y);
1357                    }
1358                    break;
1359                }
1360            }
1361            
1362            if let Some(drop) = calculated_drop {
1363                let error = (drop - target_drop).abs();
1364                total_error += error * error;
1365            }
1366        }
1367        
1368        if total_error < best_error {
1369            best_error = total_error;
1370            best_bc = bc_value;
1371            found_valid = true;
1372        }
1373    }
1374    
1375    if !found_valid {
1376        return Err(BallisticsError::from("Unable to estimate BC from provided data. Check that drop values are in correct units.".to_string()));
1377    }
1378    
1379    Ok(best_bc)
1380}
1381
1382// Helper function to calculate air density
1383fn calculate_air_density(atmosphere: &AtmosphericConditions) -> f64 {
1384    // Simplified air density calculation
1385    // P / (R * T) where R is specific gas constant for dry air
1386    let r_specific = 287.058; // J/(kg·K)
1387    let temperature_k = atmosphere.temperature + 273.15;
1388    
1389    // Convert pressure from hPa to Pa
1390    let pressure_pa = atmosphere.pressure * 100.0;
1391    
1392    // Basic density calculation
1393    let density = pressure_pa / (r_specific * temperature_k);
1394    
1395    // Altitude correction (simplified)
1396    let altitude_factor = (-atmosphere.altitude / 8000.0).exp();
1397    
1398    density * altitude_factor
1399}
1400
1401// Add rand dependencies for Monte Carlo
1402use rand;
1403use rand_distr;