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: 0.0,  // Default 0 - height is in sight_height
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.cos(),  // X: forward component (downrange)
364            self.inputs.muzzle_velocity * self.inputs.muzzle_angle.sin(),  // Y: vertical component
365            horizontal_velocity * self.inputs.azimuth_angle.sin(),  // Z: side deviation (lateral)
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 (X is downrange, Z is lateral)
393        let wind_vector = Vector3::new(
394            self.wind.speed * self.wind.direction.cos(),  // X: downrange (head/tail wind)
395            0.0,
396            self.wind.speed * self.wind.direction.sin(),  // Z: lateral (crosswind)
397        );
398        
399        // Main integration loop (X is downrange)
400        while position.x < 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            // Debug: Log first and every 100th point
413            if points.len() == 1 || points.len() % 100 == 0 {
414                eprintln!("Trajectory point {}: time={:.3}s, x={:.2}m, y={:.2}m, z={:.2}m, vel={:.1}m/s",
415                    points.len(), time, position.x, position.y, position.z, velocity_magnitude);
416            }
417            
418            // Track max height
419            if position.y > max_height {
420                max_height = position.y;
421            }
422            
423            // Calculate pitch damping if enabled
424            if self.inputs.enable_pitch_damping {
425                let temp_c = self.atmosphere.temperature;
426                let temp_k = temp_c + 273.15;
427                let speed_of_sound = (1.4 * 287.05 * temp_k).sqrt();
428                let mach = velocity_magnitude / speed_of_sound;
429                
430                // Track when we enter transonic
431                if transonic_mach.is_none() && mach < 1.2 && mach > 0.8 {
432                    transonic_mach = Some(mach);
433                }
434                
435                // Calculate pitch damping coefficient
436                let bullet_type = if let Some(ref model) = self.inputs.bullet_model {
437                    model.as_str()
438                } else {
439                    "default"
440                };
441                let coeffs = PitchDampingCoefficients::from_bullet_type(bullet_type);
442                let pitch_damping = calculate_pitch_damping_coefficient(mach, &coeffs);
443                
444                // Track minimum (most critical for stability)
445                if pitch_damping < min_pitch_damping {
446                    min_pitch_damping = pitch_damping;
447                }
448            }
449            
450            // Calculate precession/nutation if enabled
451            if self.inputs.enable_precession_nutation {
452                if let Some(ref mut state) = angular_state {
453                    let velocity_magnitude = velocity.magnitude();
454                    let temp_c = self.atmosphere.temperature;
455                    let temp_k = temp_c + 273.15;
456                    let speed_of_sound = (1.4 * 287.05 * temp_k).sqrt();
457                    let mach = velocity_magnitude / speed_of_sound;
458                    
459                    // Calculate spin rate from twist rate and velocity
460                    let spin_rate_rad_s = if self.inputs.twist_rate > 0.0 {
461                        let velocity_fps = velocity_magnitude * 3.28084;
462                        let twist_rate_ft = self.inputs.twist_rate / 12.0;
463                        (velocity_fps / twist_rate_ft) * 2.0 * std::f64::consts::PI
464                    } else {
465                        0.0
466                    };
467                    
468                    // Create precession/nutation parameters
469                    let params = PrecessionNutationParams {
470                        mass_kg: self.inputs.bullet_mass,
471                        caliber_m: self.inputs.bullet_diameter,
472                        length_m: self.inputs.bullet_length,
473                        spin_rate_rad_s,
474                        spin_inertia: 6.94e-8,      // Typical value
475                        transverse_inertia: 9.13e-7, // Typical value
476                        velocity_mps: velocity_magnitude,
477                        air_density_kg_m3: air_density,
478                        mach,
479                        pitch_damping_coeff: -0.8,
480                        nutation_damping_factor: 0.05,
481                    };
482                    
483                    // Update angular state
484                    *state = calculate_combined_angular_motion(
485                        &params,
486                        state,
487                        time,
488                        self.time_step,
489                        0.001,  // Initial disturbance
490                    );
491                    
492                    // Track maximums
493                    if state.yaw_angle.abs() > max_yaw_angle {
494                        max_yaw_angle = state.yaw_angle.abs();
495                    }
496                    if state.precession_angle.abs() > max_precession_angle {
497                        max_precession_angle = state.precession_angle.abs();
498                    }
499                }
500            }
501            
502            // Calculate drag with altitude-dependent wind if enabled
503            let actual_wind = if self.inputs.enable_wind_shear {
504                self.get_wind_at_altitude(position.y)
505            } else {
506                wind_vector.clone()
507            };
508            let velocity_rel = velocity - actual_wind;
509            let velocity_rel_mag = velocity_rel.magnitude();
510            let drag_coefficient = self.calculate_drag_coefficient(velocity_rel_mag);
511            
512            // Calculate drag force
513            let drag_force = 0.5 * air_density * drag_coefficient * 
514                            self.inputs.bullet_diameter * self.inputs.bullet_diameter * 
515                            std::f64::consts::PI / 4.0 * velocity_rel_mag * velocity_rel_mag;
516            
517            // Calculate acceleration
518            let drag_acceleration = -drag_force / self.inputs.bullet_mass;
519            let acceleration = Vector3::new(
520                drag_acceleration * velocity_rel.x / velocity_rel_mag,
521                drag_acceleration * velocity_rel.y / velocity_rel_mag - 9.80665,
522                drag_acceleration * velocity_rel.z / velocity_rel_mag,
523            );
524            
525            // Update state
526            velocity += acceleration * self.time_step;
527            position += velocity * self.time_step;
528            time += self.time_step;
529        }
530        
531        // Get final values
532        let last_point = points.last().ok_or("No trajectory points generated")?;
533        
534        // Create trajectory sampling data if enabled
535        let sampled_points = if self.inputs.enable_trajectory_sampling {
536            let trajectory_data = TrajectoryData {
537                times: points.iter().map(|p| p.time).collect(),
538                positions: points.iter().map(|p| p.position.clone()).collect(),
539                velocities: points.iter().map(|p| {
540                    // Reconstruct velocity vectors from magnitude (approximate)
541                    Vector3::new(0.0, 0.0, p.velocity_magnitude)
542                }).collect(),
543                transonic_distances: Vec::new(),  // TODO: Track Mach transitions
544            };
545            
546            let outputs = TrajectoryOutputs {
547                target_distance_horiz_m: last_point.position.x,  // X is downrange
548                target_vertical_height_m: last_point.position.y,
549                time_of_flight_s: last_point.time,
550                max_ord_dist_horiz_m: max_height,
551            };
552            
553            // Sample at specified intervals
554            let samples = sample_trajectory(&trajectory_data, &outputs, self.inputs.sample_interval, self.inputs.bullet_mass);
555            Some(samples)
556        } else {
557            None
558        };
559        
560        Ok(TrajectoryResult {
561            max_range: last_point.position.x,  // X is downrange
562            max_height,
563            time_of_flight: last_point.time,
564            impact_velocity: last_point.velocity_magnitude,
565            impact_energy: last_point.kinetic_energy,
566            points,
567            sampled_points,
568            min_pitch_damping: if self.inputs.enable_pitch_damping { Some(min_pitch_damping) } else { None },
569            transonic_mach: transonic_mach,
570            angular_state: angular_state,
571            max_yaw_angle: if self.inputs.enable_precession_nutation { Some(max_yaw_angle) } else { None },
572            max_precession_angle: if self.inputs.enable_precession_nutation { Some(max_precession_angle) } else { None },
573        })
574    }
575    
576    fn solve_rk4(&self) -> Result<TrajectoryResult, BallisticsError> {
577        // RK4 trajectory integration for better accuracy
578        let mut time = 0.0;
579        let mut position = Vector3::new(0.0, self.inputs.sight_height + self.inputs.muzzle_height, 0.0);
580        
581        // Calculate initial velocity components with both elevation and azimuth
582        let horizontal_velocity = self.inputs.muzzle_velocity * self.inputs.muzzle_angle.cos();
583        let mut velocity = Vector3::new(
584            horizontal_velocity * self.inputs.azimuth_angle.cos(),  // X: forward component (downrange)
585            self.inputs.muzzle_velocity * self.inputs.muzzle_angle.sin(),  // Y: vertical component
586            horizontal_velocity * self.inputs.azimuth_angle.sin(),  // Z: side deviation (lateral)
587        );
588
589        let mut points = Vec::new();
590        let mut max_height = position.y;
591        let mut min_pitch_damping = 1.0;  // Track minimum pitch damping coefficient
592        let mut transonic_mach = None;    // Track when we enter transonic
593
594        // Initialize angular state for precession/nutation tracking
595        let mut angular_state = if self.inputs.enable_precession_nutation {
596            Some(AngularState {
597                pitch_angle: 0.001,  // Small initial disturbance
598                yaw_angle: 0.001,
599                pitch_rate: 0.0,
600                yaw_rate: 0.0,
601                precession_angle: 0.0,
602                nutation_phase: 0.0,
603            })
604        } else {
605            None
606        };
607        let mut max_yaw_angle = 0.0;
608        let mut max_precession_angle = 0.0;
609
610        // Calculate air density
611        let air_density = calculate_air_density(&self.atmosphere);
612
613        // Wind vector (X is downrange, Z is lateral)
614        let wind_vector = Vector3::new(
615            self.wind.speed * self.wind.direction.cos(),  // X: downrange (head/tail wind)
616            0.0,
617            self.wind.speed * self.wind.direction.sin(),  // Z: lateral (crosswind)
618        );
619
620        // Main RK4 integration loop (X is downrange)
621        while position.x < self.max_range && position.y >= 0.0 && time < 100.0 {
622            // Store trajectory point
623            let velocity_magnitude = velocity.magnitude();
624            let kinetic_energy = 0.5 * self.inputs.bullet_mass * velocity_magnitude * velocity_magnitude;
625            
626            points.push(TrajectoryPoint {
627                time,
628                position: position.clone(),
629                velocity_magnitude,
630                kinetic_energy,
631            });
632            
633            if position.y > max_height {
634                max_height = position.y;
635            }
636            
637            // Calculate pitch damping if enabled (RK4 solver)
638            if self.inputs.enable_pitch_damping {
639                let temp_c = self.atmosphere.temperature;
640                let temp_k = temp_c + 273.15;
641                let speed_of_sound = (1.4 * 287.05 * temp_k).sqrt();
642                let mach = velocity_magnitude / speed_of_sound;
643                
644                // Track when we enter transonic
645                if transonic_mach.is_none() && mach < 1.2 && mach > 0.8 {
646                    transonic_mach = Some(mach);
647                }
648                
649                // Calculate pitch damping coefficient
650                let bullet_type = if let Some(ref model) = self.inputs.bullet_model {
651                    model.as_str()
652                } else {
653                    "default"
654                };
655                let coeffs = PitchDampingCoefficients::from_bullet_type(bullet_type);
656                let pitch_damping = calculate_pitch_damping_coefficient(mach, &coeffs);
657                
658                // Track minimum (most critical for stability)
659                if pitch_damping < min_pitch_damping {
660                    min_pitch_damping = pitch_damping;
661                }
662            }
663            
664            // Calculate precession/nutation if enabled (RK4 solver)
665            if self.inputs.enable_precession_nutation {
666                if let Some(ref mut state) = angular_state {
667                    let velocity_magnitude = velocity.magnitude();
668                    let temp_c = self.atmosphere.temperature;
669                    let temp_k = temp_c + 273.15;
670                    let speed_of_sound = (1.4 * 287.05 * temp_k).sqrt();
671                    let mach = velocity_magnitude / speed_of_sound;
672                    
673                    // Calculate spin rate from twist rate and velocity
674                    let spin_rate_rad_s = if self.inputs.twist_rate > 0.0 {
675                        let velocity_fps = velocity_magnitude * 3.28084;
676                        let twist_rate_ft = self.inputs.twist_rate / 12.0;
677                        (velocity_fps / twist_rate_ft) * 2.0 * std::f64::consts::PI
678                    } else {
679                        0.0
680                    };
681                    
682                    // Create precession/nutation parameters
683                    let params = PrecessionNutationParams {
684                        mass_kg: self.inputs.bullet_mass,
685                        caliber_m: self.inputs.bullet_diameter,
686                        length_m: self.inputs.bullet_length,
687                        spin_rate_rad_s,
688                        spin_inertia: 6.94e-8,      // Typical value
689                        transverse_inertia: 9.13e-7, // Typical value
690                        velocity_mps: velocity_magnitude,
691                        air_density_kg_m3: air_density,
692                        mach,
693                        pitch_damping_coeff: -0.8,
694                        nutation_damping_factor: 0.05,
695                    };
696                    
697                    // Update angular state
698                    *state = calculate_combined_angular_motion(
699                        &params,
700                        state,
701                        time,
702                        self.time_step,
703                        0.001,  // Initial disturbance
704                    );
705                    
706                    // Track maximums
707                    if state.yaw_angle.abs() > max_yaw_angle {
708                        max_yaw_angle = state.yaw_angle.abs();
709                    }
710                    if state.precession_angle.abs() > max_precession_angle {
711                        max_precession_angle = state.precession_angle.abs();
712                    }
713                }
714            }
715            
716            // RK4 method
717            let dt = self.time_step;
718            
719            // k1
720            let acc1 = self.calculate_acceleration(&position, &velocity, air_density, &wind_vector);
721            
722            // k2
723            let pos2 = position + velocity * (dt * 0.5);
724            let vel2 = velocity + acc1 * (dt * 0.5);
725            let acc2 = self.calculate_acceleration(&pos2, &vel2, air_density, &wind_vector);
726            
727            // k3
728            let pos3 = position + vel2 * (dt * 0.5);
729            let vel3 = velocity + acc2 * (dt * 0.5);
730            let acc3 = self.calculate_acceleration(&pos3, &vel3, air_density, &wind_vector);
731            
732            // k4
733            let pos4 = position + vel3 * dt;
734            let vel4 = velocity + acc3 * dt;
735            let acc4 = self.calculate_acceleration(&pos4, &vel4, air_density, &wind_vector);
736            
737            // Update position and velocity
738            position += (velocity + vel2 * 2.0 + vel3 * 2.0 + vel4) * (dt / 6.0);
739            velocity += (acc1 + acc2 * 2.0 + acc3 * 2.0 + acc4) * (dt / 6.0);
740            time += dt;
741        }
742        
743        // Get final values
744        let last_point = points.last().ok_or("No trajectory points generated")?;
745        
746        // Create trajectory sampling data if enabled
747        let sampled_points = if self.inputs.enable_trajectory_sampling {
748            let trajectory_data = TrajectoryData {
749                times: points.iter().map(|p| p.time).collect(),
750                positions: points.iter().map(|p| p.position.clone()).collect(),
751                velocities: points.iter().map(|p| {
752                    // Reconstruct velocity vectors from magnitude (approximate)
753                    Vector3::new(0.0, 0.0, p.velocity_magnitude)
754                }).collect(),
755                transonic_distances: Vec::new(),  // TODO: Track Mach transitions
756            };
757            
758            let outputs = TrajectoryOutputs {
759                target_distance_horiz_m: last_point.position.x,  // X is downrange
760                target_vertical_height_m: last_point.position.y,
761                time_of_flight_s: last_point.time,
762                max_ord_dist_horiz_m: max_height,
763            };
764            
765            // Sample at specified intervals
766            let samples = sample_trajectory(&trajectory_data, &outputs, self.inputs.sample_interval, self.inputs.bullet_mass);
767            Some(samples)
768        } else {
769            None
770        };
771        
772        Ok(TrajectoryResult {
773            max_range: last_point.position.x,  // X is downrange
774            max_height,
775            time_of_flight: last_point.time,
776            impact_velocity: last_point.velocity_magnitude,
777            impact_energy: last_point.kinetic_energy,
778            points,
779            sampled_points,
780            min_pitch_damping: if self.inputs.enable_pitch_damping { Some(min_pitch_damping) } else { None },
781            transonic_mach: transonic_mach,
782            angular_state: angular_state,
783            max_yaw_angle: if self.inputs.enable_precession_nutation { Some(max_yaw_angle) } else { None },
784            max_precession_angle: if self.inputs.enable_precession_nutation { Some(max_precession_angle) } else { None },
785        })
786    }
787    
788    fn solve_rk45(&self) -> Result<TrajectoryResult, BallisticsError> {
789        // RK45 adaptive step size integration (Dormand-Prince method)
790        let mut time = 0.0;
791        let mut position = Vector3::new(0.0, self.inputs.sight_height + self.inputs.muzzle_height, 0.0);
792        
793        // Calculate initial velocity components (X is downrange, Z is lateral)
794        let horizontal_velocity = self.inputs.muzzle_velocity * self.inputs.muzzle_angle.cos();
795        let mut velocity = Vector3::new(
796            horizontal_velocity * self.inputs.azimuth_angle.cos(),  // X: forward component (downrange)
797            self.inputs.muzzle_velocity * self.inputs.muzzle_angle.sin(),  // Y: vertical component
798            horizontal_velocity * self.inputs.azimuth_angle.sin(),  // Z: side deviation (lateral)
799        );
800        
801        let mut points = Vec::new();
802        let mut max_height = position.y;
803        let mut dt = 0.001;  // Initial step size
804        let tolerance = 1e-6;  // Error tolerance
805        let safety_factor = 0.9;  // Safety factor for step size adjustment
806        let max_dt = 0.01;  // Maximum step size
807        let min_dt = 1e-6;   // Minimum step size
808        
809        // Add a point counter to debug
810        let mut iteration_count = 0;
811        const MAX_ITERATIONS: usize = 100000;
812        
813        while position.x < self.max_range && position.y > self.inputs.ground_threshold && time < 100.0 {  // X is downrange
814            iteration_count += 1;
815            if iteration_count > MAX_ITERATIONS {
816                break; // Prevent infinite loop
817            }
818            
819            // Store current point
820            let velocity_magnitude = velocity.magnitude();
821            let kinetic_energy = 0.5 * self.inputs.bullet_mass * velocity_magnitude.powi(2);
822            
823            points.push(TrajectoryPoint {
824                time,
825                position: position.clone(),
826                velocity_magnitude,
827                kinetic_energy,
828            });
829            
830            if position.y > max_height {
831                max_height = position.y;
832            }
833            
834            // Get atmospheric conditions and wind (X is downrange, Z is lateral)
835            let air_density = calculate_air_density(&self.atmosphere);
836            let wind_vector = Vector3::new(
837                self.wind.speed * self.wind.direction.cos(),  // X: downrange (head/tail wind)
838                0.0,
839                self.wind.speed * self.wind.direction.sin(),  // Z: lateral (crosswind)
840            );
841            
842            // RK45 step with adaptive step size
843            let (new_pos, new_vel, new_dt) = self.rk45_step(
844                &position,
845                &velocity,
846                dt,
847                air_density,
848                &wind_vector,
849                tolerance,
850            );
851            
852            // Update step size with safety factor and bounds
853            dt = (safety_factor * new_dt).clamp(min_dt, max_dt);
854            
855            // Update state
856            position = new_pos;
857            velocity = new_vel;
858            time += dt;
859        }
860        
861        // Ensure we have at least one point
862        if points.is_empty() {
863            return Err(BallisticsError::from("No trajectory points calculated"));
864        }
865
866        let last_point = points.last().unwrap();
867
868        Ok(TrajectoryResult {
869            max_range: last_point.position.x,  // X is downrange
870            max_height,
871            time_of_flight: last_point.time,
872            impact_velocity: last_point.velocity_magnitude,
873            impact_energy: last_point.kinetic_energy,
874            points,
875            sampled_points: None,  // Simplified - no trajectory sampling in RK45 for now
876            min_pitch_damping: None,
877            transonic_mach: None,
878            angular_state: None,
879            max_yaw_angle: None,
880            max_precession_angle: None,
881        })
882    }
883    
884    fn rk45_step(
885        &self,
886        position: &Vector3<f64>,
887        velocity: &Vector3<f64>,
888        dt: f64,
889        air_density: f64,
890        wind_vector: &Vector3<f64>,
891        tolerance: f64,
892    ) -> (Vector3<f64>, Vector3<f64>, f64) {
893        // Dormand-Prince coefficients
894        const A21: f64 = 1.0 / 5.0;
895        const A31: f64 = 3.0 / 40.0;
896        const A32: f64 = 9.0 / 40.0;
897        const A41: f64 = 44.0 / 45.0;
898        const A42: f64 = -56.0 / 15.0;
899        const A43: f64 = 32.0 / 9.0;
900        const A51: f64 = 19372.0 / 6561.0;
901        const A52: f64 = -25360.0 / 2187.0;
902        const A53: f64 = 64448.0 / 6561.0;
903        const A54: f64 = -212.0 / 729.0;
904        const A61: f64 = 9017.0 / 3168.0;
905        const A62: f64 = -355.0 / 33.0;
906        const A63: f64 = 46732.0 / 5247.0;
907        const A64: f64 = 49.0 / 176.0;
908        const A65: f64 = -5103.0 / 18656.0;
909        const A71: f64 = 35.0 / 384.0;
910        const A73: f64 = 500.0 / 1113.0;
911        const A74: f64 = 125.0 / 192.0;
912        const A75: f64 = -2187.0 / 6784.0;
913        const A76: f64 = 11.0 / 84.0;
914        
915        // 5th order coefficients
916        const B1: f64 = 35.0 / 384.0;
917        const B3: f64 = 500.0 / 1113.0;
918        const B4: f64 = 125.0 / 192.0;
919        const B5: f64 = -2187.0 / 6784.0;
920        const B6: f64 = 11.0 / 84.0;
921        
922        // 4th order coefficients for error estimation
923        const B1_ERR: f64 = 5179.0 / 57600.0;
924        const B3_ERR: f64 = 7571.0 / 16695.0;
925        const B4_ERR: f64 = 393.0 / 640.0;
926        const B5_ERR: f64 = -92097.0 / 339200.0;
927        const B6_ERR: f64 = 187.0 / 2100.0;
928        const B7_ERR: f64 = 1.0 / 40.0;
929        
930        // Compute RK45 stages
931        let k1_v = self.calculate_acceleration(position, velocity, air_density, wind_vector);
932        let k1_p = velocity.clone();
933        
934        let p2 = position + dt * A21 * k1_p;
935        let v2 = velocity + dt * A21 * k1_v;
936        let k2_v = self.calculate_acceleration(&p2, &v2, air_density, wind_vector);
937        let k2_p = v2;
938        
939        let p3 = position + dt * (A31 * k1_p + A32 * k2_p);
940        let v3 = velocity + dt * (A31 * k1_v + A32 * k2_v);
941        let k3_v = self.calculate_acceleration(&p3, &v3, air_density, wind_vector);
942        let k3_p = v3;
943        
944        let p4 = position + dt * (A41 * k1_p + A42 * k2_p + A43 * k3_p);
945        let v4 = velocity + dt * (A41 * k1_v + A42 * k2_v + A43 * k3_v);
946        let k4_v = self.calculate_acceleration(&p4, &v4, air_density, wind_vector);
947        let k4_p = v4;
948        
949        let p5 = position + dt * (A51 * k1_p + A52 * k2_p + A53 * k3_p + A54 * k4_p);
950        let v5 = velocity + dt * (A51 * k1_v + A52 * k2_v + A53 * k3_v + A54 * k4_v);
951        let k5_v = self.calculate_acceleration(&p5, &v5, air_density, wind_vector);
952        let k5_p = v5;
953        
954        let p6 = position + dt * (A61 * k1_p + A62 * k2_p + A63 * k3_p + A64 * k4_p + A65 * k5_p);
955        let v6 = velocity + dt * (A61 * k1_v + A62 * k2_v + A63 * k3_v + A64 * k4_v + A65 * k5_v);
956        let k6_v = self.calculate_acceleration(&p6, &v6, air_density, wind_vector);
957        let k6_p = v6;
958        
959        let p7 = position + dt * (A71 * k1_p + A73 * k3_p + A74 * k4_p + A75 * k5_p + A76 * k6_p);
960        let v7 = velocity + dt * (A71 * k1_v + A73 * k3_v + A74 * k4_v + A75 * k5_v + A76 * k6_v);
961        let k7_v = self.calculate_acceleration(&p7, &v7, air_density, wind_vector);
962        let k7_p = v7;
963        
964        // 5th order solution
965        let new_pos = position + dt * (B1 * k1_p + B3 * k3_p + B4 * k4_p + B5 * k5_p + B6 * k6_p);
966        let new_vel = velocity + dt * (B1 * k1_v + B3 * k3_v + B4 * k4_v + B5 * k5_v + B6 * k6_v);
967        
968        // 4th order solution for error estimate
969        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);
970        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);
971        
972        // Estimate error
973        let pos_error = (new_pos - pos_err).magnitude();
974        let vel_error = (new_vel - vel_err).magnitude();
975        let error = (pos_error + vel_error) / (1.0 + position.magnitude() + velocity.magnitude());
976        
977        // Calculate new step size
978        let dt_new = if error < tolerance {
979            dt * (tolerance / error).powf(0.2).min(2.0)
980        } else {
981            dt * (tolerance / error).powf(0.25).max(0.1)
982        };
983        
984        (new_pos, new_vel, dt_new)
985    }
986    
987    fn calculate_acceleration(&self, position: &Vector3<f64>, velocity: &Vector3<f64>, air_density: f64, wind_vector: &Vector3<f64>) -> Vector3<f64> {
988        // Calculate altitude-dependent wind if wind shear is enabled
989        let actual_wind = if self.inputs.enable_wind_shear {
990            self.get_wind_at_altitude(position.y)
991        } else {
992            wind_vector.clone()
993        };
994        
995        let relative_velocity = velocity - &actual_wind;
996        let velocity_magnitude = relative_velocity.magnitude();
997        
998        if velocity_magnitude < 0.001 {
999            return Vector3::new(0.0, -9.81, 0.0);
1000        }
1001        
1002        // Calculate drag force
1003        let cd = self.calculate_drag_coefficient(velocity_magnitude);
1004        let reference_area = std::f64::consts::PI * (self.inputs.bullet_diameter / 2.0).powi(2);
1005        
1006        // Apply cluster BC correction if enabled
1007        let effective_bc = if let Some(ref cluster_bc) = self.cluster_bc {
1008            // Convert velocity to fps for cluster BC calculation
1009            let velocity_fps = velocity_magnitude * 3.28084;
1010            cluster_bc.apply_correction(
1011                self.inputs.bc_value,
1012                self.inputs.caliber_inches * 0.0254,  // Convert back to meters for consistency
1013                self.inputs.weight_grains,
1014                velocity_fps
1015            )
1016        } else {
1017            self.inputs.bc_value
1018        };
1019        
1020        let drag_magnitude = 0.5 * air_density * velocity_magnitude.powi(2) * cd * reference_area / effective_bc;
1021        
1022        // Drag acts opposite to velocity
1023        let drag_force = -relative_velocity.normalize() * drag_magnitude;
1024        
1025        // Total acceleration = drag/mass + gravity
1026        let acceleration = drag_force / self.inputs.bullet_mass + Vector3::new(0.0, -9.81, 0.0);
1027        
1028        acceleration
1029    }
1030    
1031    fn calculate_drag_coefficient(&self, velocity: f64) -> f64 {
1032        // Calculate speed of sound based on atmospheric temperature
1033        // Use standard atmosphere temperature at sea level if not available
1034        let temp_c = self.atmosphere.temperature;
1035        let temp_k = temp_c + 273.15;
1036        let gamma = 1.4;  // Ratio of specific heats for air
1037        let r_specific = 287.05;  // Specific gas constant for air (J/kg·K)
1038        let speed_of_sound = (gamma * r_specific * temp_k).sqrt();
1039        let mach = velocity / speed_of_sound;
1040        
1041        // Base drag coefficient from drag model
1042        let base_cd = match self.inputs.bc_type {
1043            DragModel::G1 => 0.5,
1044            DragModel::G7 => 0.4,
1045            _ => 0.45,
1046        };
1047        
1048        // Determine projectile shape for transonic corrections
1049        let projectile_shape = if let Some(ref model) = self.inputs.bullet_model {
1050            // Try to determine shape from bullet model string
1051            if model.to_lowercase().contains("boat") || model.to_lowercase().contains("bt") {
1052                ProjectileShape::BoatTail
1053            } else if model.to_lowercase().contains("round") || model.to_lowercase().contains("rn") {
1054                ProjectileShape::RoundNose
1055            } else if model.to_lowercase().contains("flat") || model.to_lowercase().contains("fb") {
1056                ProjectileShape::FlatBase
1057            } else {
1058                // Use heuristic based on caliber, weight, and drag model
1059                get_projectile_shape(
1060                    self.inputs.bullet_diameter,
1061                    self.inputs.bullet_mass / 0.00006479891,  // Convert kg to grains
1062                    &self.inputs.bc_type.to_string()
1063                )
1064            }
1065        } else {
1066            // Use heuristic based on caliber, weight, and drag model
1067            get_projectile_shape(
1068                self.inputs.bullet_diameter,
1069                self.inputs.bullet_mass / 0.00006479891,  // Convert kg to grains
1070                &self.inputs.bc_type.to_string()
1071            )
1072        };
1073        
1074        // Apply transonic corrections
1075        // Enable wave drag if advanced effects are enabled
1076        let include_wave_drag = self.inputs.enable_advanced_effects;
1077        transonic_correction(mach, base_cd, projectile_shape, include_wave_drag)
1078    }
1079}
1080
1081// Monte Carlo parameters
1082#[derive(Debug, Clone)]
1083pub struct MonteCarloParams {
1084    pub num_simulations: usize,
1085    pub velocity_std_dev: f64,
1086    pub angle_std_dev: f64,
1087    pub bc_std_dev: f64,
1088    pub wind_speed_std_dev: f64,
1089    pub target_distance: Option<f64>,
1090    pub base_wind_speed: f64,
1091    pub base_wind_direction: f64,
1092    pub azimuth_std_dev: f64,  // Horizontal aiming variation in radians
1093}
1094
1095impl Default for MonteCarloParams {
1096    fn default() -> Self {
1097        Self {
1098            num_simulations: 1000,
1099            velocity_std_dev: 1.0,
1100            angle_std_dev: 0.001,
1101            bc_std_dev: 0.01,
1102            wind_speed_std_dev: 1.0,
1103            target_distance: None,
1104            base_wind_speed: 0.0,
1105            base_wind_direction: 0.0,
1106            azimuth_std_dev: 0.001,  // Default horizontal spread ~0.057 degrees
1107        }
1108    }
1109}
1110
1111// Monte Carlo results
1112#[derive(Debug, Clone)]
1113pub struct MonteCarloResults {
1114    pub ranges: Vec<f64>,
1115    pub impact_velocities: Vec<f64>,
1116    pub impact_positions: Vec<Vector3<f64>>,
1117}
1118
1119// Run Monte Carlo simulation (backwards compatibility)
1120pub fn run_monte_carlo(
1121    base_inputs: BallisticInputs,
1122    params: MonteCarloParams,
1123) -> Result<MonteCarloResults, BallisticsError> {
1124    let base_wind = WindConditions {
1125        speed: params.base_wind_speed,
1126        direction: params.base_wind_direction,
1127    };
1128    run_monte_carlo_with_wind(base_inputs, base_wind, params)
1129}
1130
1131// Run Monte Carlo simulation with wind
1132pub fn run_monte_carlo_with_wind(
1133    base_inputs: BallisticInputs,
1134    base_wind: WindConditions,
1135    params: MonteCarloParams,
1136) -> Result<MonteCarloResults, BallisticsError> {
1137    use rand::thread_rng;
1138    use rand_distr::{Distribution, Normal};
1139    
1140    let mut rng = thread_rng();
1141    let mut ranges = Vec::new();
1142    let mut impact_velocities = Vec::new();
1143    let mut impact_positions = Vec::new();
1144    
1145    // First, calculate baseline trajectory with no variations
1146    let baseline_solver = TrajectorySolver::new(base_inputs.clone(), base_wind.clone(), Default::default());
1147    let baseline_result = baseline_solver.solve()?;
1148    let baseline_impact = baseline_result.points.last()
1149        .ok_or("No baseline trajectory points")?
1150        .position.clone();
1151    
1152    // Create normal distributions for variations
1153    let velocity_dist = Normal::new(base_inputs.muzzle_velocity, params.velocity_std_dev)
1154        .map_err(|e| format!("Invalid velocity distribution: {}", e))?;
1155    let angle_dist = Normal::new(base_inputs.muzzle_angle, params.angle_std_dev)
1156        .map_err(|e| format!("Invalid angle distribution: {}", e))?;
1157    let bc_dist = Normal::new(base_inputs.bc_value, params.bc_std_dev)
1158        .map_err(|e| format!("Invalid BC distribution: {}", e))?;
1159    let wind_speed_dist = Normal::new(base_wind.speed, params.wind_speed_std_dev)
1160        .map_err(|e| format!("Invalid wind speed distribution: {}", e))?;
1161    let wind_dir_dist = Normal::new(base_wind.direction, params.wind_speed_std_dev * 0.1)  // Small variation in direction
1162        .map_err(|e| format!("Invalid wind direction distribution: {}", e))?;
1163    let azimuth_dist = Normal::new(base_inputs.azimuth_angle, params.azimuth_std_dev)
1164        .map_err(|e| format!("Invalid azimuth distribution: {}", e))?;
1165    
1166    // Create distribution for pointing errors (simulates shooter's aiming consistency)
1167    let distance = baseline_impact.z;  // Distance to target
1168    let pointing_error_dist = Normal::new(0.0, params.angle_std_dev * distance)
1169        .map_err(|e| format!("Invalid pointing distribution: {}", e))?;
1170    
1171    for _ in 0..params.num_simulations {
1172        // Create varied inputs
1173        let mut inputs = base_inputs.clone();
1174        inputs.muzzle_velocity = velocity_dist.sample(&mut rng).max(0.0);
1175        inputs.muzzle_angle = angle_dist.sample(&mut rng);
1176        inputs.bc_value = bc_dist.sample(&mut rng).max(0.01);
1177        inputs.azimuth_angle = azimuth_dist.sample(&mut rng);  // Add horizontal variation
1178        
1179        // Create varied wind (now based on base wind conditions)
1180        let wind = WindConditions {
1181            speed: wind_speed_dist.sample(&mut rng).abs(),
1182            direction: wind_dir_dist.sample(&mut rng),
1183        };
1184        
1185        // Run trajectory
1186        let solver = TrajectorySolver::new(inputs, wind, Default::default());
1187        match solver.solve() {
1188            Ok(result) => {
1189                ranges.push(result.max_range);
1190                impact_velocities.push(result.impact_velocity);
1191                if let Some(last_point) = result.points.last() {
1192                    // Calculate physical deviation from baseline impact point
1193                    let mut deviation = Vector3::new(
1194                        last_point.position.x - baseline_impact.x,  // Lateral deviation
1195                        last_point.position.y - baseline_impact.y,  // Vertical deviation
1196                        last_point.position.z - baseline_impact.z,  // Range deviation
1197                    );
1198                    
1199                    // Add additional pointing error to simulate realistic group sizes
1200                    // This represents the shooter's ability to aim consistently
1201                    let pointing_error_y = pointing_error_dist.sample(&mut rng);
1202                    deviation.y += pointing_error_y;
1203                    
1204                    impact_positions.push(deviation);
1205                }
1206            },
1207            Err(_) => {
1208                // Skip failed simulations
1209                continue;
1210            }
1211        }
1212    }
1213    
1214    if ranges.is_empty() {
1215        return Err("No successful simulations".into());
1216    }
1217    
1218    Ok(MonteCarloResults {
1219        ranges,
1220        impact_velocities,
1221        impact_positions,
1222    })
1223}
1224
1225// Calculate zero angle for a target
1226pub fn calculate_zero_angle(
1227    inputs: BallisticInputs,
1228    target_distance: f64,
1229    target_height: f64,
1230) -> Result<f64, BallisticsError> {
1231    calculate_zero_angle_with_conditions(
1232        inputs,
1233        target_distance,
1234        target_height,
1235        WindConditions::default(),
1236        AtmosphericConditions::default(),
1237    )
1238}
1239
1240pub fn calculate_zero_angle_with_conditions(
1241    inputs: BallisticInputs,
1242    target_distance: f64,
1243    target_height: f64,
1244    wind: WindConditions,
1245    atmosphere: AtmosphericConditions,
1246) -> Result<f64, BallisticsError> {
1247    // Binary search for the angle that hits the target
1248    // Use only positive angles to ensure proper ballistic arc (upward trajectory)
1249    let mut low_angle = 0.0;   // radians (horizontal)
1250    let mut high_angle = 0.2;  // radians (about 11 degrees)
1251    let tolerance = 0.00001;   // radians
1252    let max_iterations = 50;
1253    
1254    for iteration in 0..max_iterations {
1255        let mid_angle = (low_angle + high_angle) / 2.0;
1256        
1257        let mut test_inputs = inputs.clone();
1258        test_inputs.muzzle_angle = mid_angle;
1259        
1260        let mut solver = TrajectorySolver::new(test_inputs, wind.clone(), atmosphere.clone());
1261        // Make sure we calculate far enough to reach the target
1262        solver.set_max_range(target_distance * 2.0);
1263        solver.set_time_step(0.001);
1264        let result = solver.solve()?;
1265
1266        // Find the height at target distance (X is downrange)
1267        let mut height_at_target = None;
1268        for i in 0..result.points.len() {
1269            if result.points[i].position.x >= target_distance {
1270                if i > 0 {
1271                    // Linear interpolation
1272                    let p1 = &result.points[i - 1];
1273                    let p2 = &result.points[i];
1274                    let t = (target_distance - p1.position.x) / (p2.position.x - p1.position.x);
1275                    height_at_target = Some(p1.position.y + t * (p2.position.y - p1.position.y));
1276                } else {
1277                    height_at_target = Some(result.points[i].position.y);
1278                }
1279                break;
1280            }
1281        }
1282        
1283        match height_at_target {
1284            Some(height) => {
1285                let error = height - target_height;
1286                if error.abs() < 0.001 {
1287                    return Ok(mid_angle);
1288                }
1289                
1290                if error > 0.0 {
1291                    high_angle = mid_angle;
1292                } else {
1293                    low_angle = mid_angle;
1294                }
1295            },
1296            None => {
1297                // Trajectory didn't reach target distance, increase angle
1298                low_angle = mid_angle;
1299            }
1300        }
1301        
1302        if (high_angle - low_angle).abs() < tolerance {
1303            return Ok(mid_angle);
1304        }
1305    }
1306    
1307    Err("Failed to find zero angle".into())
1308}
1309
1310// Estimate BC from trajectory data
1311pub fn estimate_bc_from_trajectory(
1312    velocity: f64,
1313    mass: f64,
1314    diameter: f64,
1315    points: &[(f64, f64)], // (distance, drop) pairs
1316) -> Result<f64, BallisticsError> {
1317    // Simple BC estimation using least squares
1318    let mut best_bc = 0.5;
1319    let mut best_error = f64::MAX;
1320    let mut found_valid = false;
1321    
1322    // Try different BC values
1323    for bc in (100..1000).step_by(10) {
1324        let bc_value = bc as f64 / 1000.0;
1325        
1326        let inputs = BallisticInputs {
1327            muzzle_velocity: velocity,
1328            bc_value,
1329            bullet_mass: mass,
1330            bullet_diameter: diameter,
1331            ..Default::default()
1332        };
1333        
1334        let mut solver = TrajectorySolver::new(inputs, Default::default(), Default::default());
1335        // Set max range for BC estimation
1336        solver.set_max_range(points.last().map(|(d, _)| *d * 1.5).unwrap_or(1000.0));
1337        
1338        let result = match solver.solve() {
1339            Ok(r) => r,
1340            Err(_) => continue, // Skip this BC value if solve fails
1341        };
1342        
1343        // Calculate error
1344        let mut total_error = 0.0;
1345        for (target_dist, target_drop) in points {
1346            // Find drop at this distance
1347            let mut calculated_drop = None;
1348            for i in 0..result.points.len() {
1349                if result.points[i].position.z >= *target_dist {
1350                    if i > 0 {
1351                        // Linear interpolation
1352                        let p1 = &result.points[i - 1];
1353                        let p2 = &result.points[i];
1354                        let t = (target_dist - p1.position.z) / (p2.position.z - p1.position.z);
1355                        calculated_drop = Some(-(p1.position.y + t * (p2.position.y - p1.position.y)));
1356                    } else {
1357                        calculated_drop = Some(-result.points[i].position.y);
1358                    }
1359                    break;
1360                }
1361            }
1362            
1363            if let Some(drop) = calculated_drop {
1364                let error = (drop - target_drop).abs();
1365                total_error += error * error;
1366            }
1367        }
1368        
1369        if total_error < best_error {
1370            best_error = total_error;
1371            best_bc = bc_value;
1372            found_valid = true;
1373        }
1374    }
1375    
1376    if !found_valid {
1377        return Err(BallisticsError::from("Unable to estimate BC from provided data. Check that drop values are in correct units.".to_string()));
1378    }
1379    
1380    Ok(best_bc)
1381}
1382
1383// Helper function to calculate air density
1384fn calculate_air_density(atmosphere: &AtmosphericConditions) -> f64 {
1385    // Simplified air density calculation
1386    // P / (R * T) where R is specific gas constant for dry air
1387    let r_specific = 287.058; // J/(kg·K)
1388    let temperature_k = atmosphere.temperature + 273.15;
1389    
1390    // Convert pressure from hPa to Pa
1391    let pressure_pa = atmosphere.pressure * 100.0;
1392    
1393    // Basic density calculation
1394    let density = pressure_pa / (r_specific * temperature_k);
1395    
1396    // Altitude correction (simplified)
1397    let altitude_factor = (-atmosphere.altitude / 8000.0).exp();
1398    
1399    density * altitude_factor
1400}
1401
1402// Add rand dependencies for Monte Carlo
1403use rand;
1404use rand_distr;