ballistics_engine/
cli_api.rs

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