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