1use 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#[derive(Debug, Clone, Copy, PartialEq)]
15pub enum UnitSystem {
16 Imperial,
17 Metric,
18}
19
20#[derive(Debug, Clone, Copy, PartialEq)]
22pub enum OutputFormat {
23 Table,
24 Json,
25 Csv,
26}
27
28#[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#[derive(Debug, Clone)]
58pub struct BallisticInputs {
59 pub bc_value: f64, pub bc_type: DragModel, pub bullet_mass: f64, pub muzzle_velocity: f64, pub bullet_diameter: f64, pub bullet_length: f64, pub muzzle_angle: f64, pub target_distance: f64, pub azimuth_angle: f64, pub shooting_angle: f64, pub sight_height: f64, pub muzzle_height: f64, pub target_height: f64, pub ground_threshold: f64, pub altitude: f64, pub temperature: f64, pub pressure: f64, pub humidity: f64, pub latitude: Option<f64>, pub wind_speed: f64, pub wind_angle: f64, pub twist_rate: f64, pub is_twist_right: bool, pub caliber_inches: f64, pub weight_grains: f64, pub manufacturer: Option<String>, pub bullet_model: Option<String>, pub bullet_id: Option<String>, pub bullet_cluster: Option<usize>, pub use_rk4: bool, pub use_adaptive_rk45: bool, pub enable_advanced_effects: bool,
104 pub use_powder_sensitivity: bool,
105 pub powder_temp_sensitivity: f64,
106 pub powder_temp: f64, pub tipoff_yaw: f64, pub tipoff_decay_distance: f64, pub use_bc_segments: bool,
110 pub bc_segments: Option<Vec<(f64, f64)>>, pub bc_segments_data: Option<Vec<crate::BCSegmentData>>, pub use_enhanced_spin_drift: bool,
113 pub use_form_factor: bool,
114 pub enable_wind_shear: bool,
115 pub wind_shear_model: String,
116 pub enable_trajectory_sampling: bool,
117 pub sample_interval: f64, pub enable_pitch_damping: bool,
119 pub enable_precession_nutation: bool,
120 pub use_cluster_bc: bool, pub custom_drag_table: Option<crate::drag::DragTable>,
124
125 pub bc_type_str: Option<String>,
127}
128
129impl Default for BallisticInputs {
130 fn default() -> Self {
131 let mass_kg = 0.01;
132 let diameter_m = 0.00762;
133 let bc = 0.5;
134 let muzzle_angle_rad = 0.0;
135 let bc_type = DragModel::G1;
136
137 Self {
138 bc_value: bc,
140 bc_type,
141 bullet_mass: mass_kg,
142 muzzle_velocity: 800.0,
143 bullet_diameter: diameter_m,
144 bullet_length: diameter_m * 4.0, muzzle_angle: muzzle_angle_rad,
148 target_distance: 100.0,
149 azimuth_angle: 0.0,
150 shooting_angle: 0.0,
151 sight_height: 0.05,
152 muzzle_height: 0.0, target_height: 0.0, ground_threshold: -100.0, altitude: 0.0,
158 temperature: 15.0,
159 pressure: 1013.25, humidity: 0.5, latitude: None,
162
163 wind_speed: 0.0,
165 wind_angle: 0.0,
166
167 twist_rate: 12.0, is_twist_right: true,
170 caliber_inches: diameter_m / 0.0254, weight_grains: mass_kg / 0.00006479891, manufacturer: None,
173 bullet_model: None,
174 bullet_id: None,
175 bullet_cluster: None,
176
177 use_rk4: true, use_adaptive_rk45: true, enable_advanced_effects: false,
183 use_powder_sensitivity: false,
184 powder_temp_sensitivity: 0.0,
185 powder_temp: 15.0,
186 tipoff_yaw: 0.0,
187 tipoff_decay_distance: 50.0,
188 use_bc_segments: false,
189 bc_segments: None,
190 bc_segments_data: None,
191 use_enhanced_spin_drift: false,
192 use_form_factor: false,
193 enable_wind_shear: false,
194 wind_shear_model: "none".to_string(),
195 enable_trajectory_sampling: false,
196 sample_interval: 10.0, enable_pitch_damping: false,
198 enable_precession_nutation: false,
199 use_cluster_bc: false, custom_drag_table: None,
203
204 bc_type_str: None,
206 }
207 }
208}
209
210#[derive(Debug, Clone)]
212pub struct WindConditions {
213 pub speed: f64, pub direction: f64, }
216
217impl Default for WindConditions {
218 fn default() -> Self {
219 Self {
220 speed: 0.0,
221 direction: 0.0,
222 }
223 }
224}
225
226#[derive(Debug, Clone)]
228pub struct AtmosphericConditions {
229 pub temperature: f64, pub pressure: f64, pub humidity: f64, pub altitude: f64, }
234
235impl Default for AtmosphericConditions {
236 fn default() -> Self {
237 Self {
238 temperature: 15.0,
239 pressure: 1013.25,
240 humidity: 50.0,
241 altitude: 0.0,
242 }
243 }
244}
245
246#[derive(Debug, Clone)]
248pub struct TrajectoryPoint {
249 pub time: f64,
250 pub position: Vector3<f64>,
251 pub velocity_magnitude: f64,
252 pub kinetic_energy: f64,
253}
254
255#[derive(Debug, Clone)]
257pub struct TrajectoryResult {
258 pub max_range: f64,
259 pub max_height: f64,
260 pub time_of_flight: f64,
261 pub impact_velocity: f64,
262 pub impact_energy: f64,
263 pub points: Vec<TrajectoryPoint>,
264 pub sampled_points: Option<Vec<TrajectorySample>>, pub min_pitch_damping: Option<f64>, pub transonic_mach: Option<f64>, pub angular_state: Option<AngularState>, pub max_yaw_angle: Option<f64>, pub max_precession_angle: Option<f64>, }
271
272pub struct TrajectorySolver {
274 inputs: BallisticInputs,
275 wind: WindConditions,
276 atmosphere: AtmosphericConditions,
277 max_range: f64,
278 time_step: f64,
279 cluster_bc: Option<ClusterBCDegradation>,
280}
281
282impl TrajectorySolver {
283 pub fn new(mut inputs: BallisticInputs, wind: WindConditions, atmosphere: AtmosphericConditions) -> Self {
284 inputs.caliber_inches = inputs.bullet_diameter / 0.0254;
286 inputs.weight_grains = inputs.bullet_mass / 0.00006479891;
287
288 let cluster_bc = if inputs.use_cluster_bc {
290 Some(ClusterBCDegradation::new())
291 } else {
292 None
293 };
294
295 Self {
296 inputs,
297 wind,
298 atmosphere,
299 max_range: 1000.0,
300 time_step: 0.001,
301 cluster_bc,
302 }
303 }
304
305 pub fn set_max_range(&mut self, range: f64) {
306 self.max_range = range;
307 }
308
309 pub fn set_time_step(&mut self, step: f64) {
310 self.time_step = step;
311 }
312
313 fn get_wind_at_altitude(&self, altitude_m: f64) -> Vector3<f64> {
314 let profile = WindShearProfile {
316 model: if self.inputs.wind_shear_model == "logarithmic" {
317 WindShearModel::Logarithmic
318 } else if self.inputs.wind_shear_model == "power" {
319 WindShearModel::PowerLaw
320 } else {
321 WindShearModel::PowerLaw },
323 surface_wind: WindLayer {
324 altitude_m: 0.0,
325 speed_mps: self.wind.speed,
326 direction_deg: self.wind.direction.to_degrees(),
327 },
328 reference_height: 10.0, roughness_length: 0.03, power_exponent: 1.0 / 7.0, geostrophic_wind: None,
332 custom_layers: Vec::new(),
333 };
334
335 profile.get_wind_at_altitude(altitude_m)
336 }
337
338 pub fn solve(&self) -> Result<TrajectoryResult, BallisticsError> {
339 if self.inputs.use_rk4 {
340 if self.inputs.use_adaptive_rk45 {
341 self.solve_rk45()
342 } else {
343 self.solve_rk4()
344 }
345 } else {
346 self.solve_euler()
347 }
348 }
349
350 fn solve_euler(&self) -> Result<TrajectoryResult, BallisticsError> {
351 let mut time = 0.0;
353 let mut position = Vector3::new(0.0, self.inputs.sight_height + self.inputs.muzzle_height, 0.0);
354 let horizontal_velocity = self.inputs.muzzle_velocity * self.inputs.muzzle_angle.cos();
356 let mut velocity = Vector3::new(
357 horizontal_velocity * self.inputs.azimuth_angle.cos(), self.inputs.muzzle_velocity * self.inputs.muzzle_angle.sin(), horizontal_velocity * self.inputs.azimuth_angle.sin(), );
361
362 let mut points = Vec::new();
363 let mut max_height = position.y;
364 let mut min_pitch_damping = 1.0; let mut transonic_mach = None; let mut angular_state = if self.inputs.enable_precession_nutation {
369 Some(AngularState {
370 pitch_angle: 0.001, yaw_angle: 0.001,
372 pitch_rate: 0.0,
373 yaw_rate: 0.0,
374 precession_angle: 0.0,
375 nutation_phase: 0.0,
376 })
377 } else {
378 None
379 };
380 let mut max_yaw_angle = 0.0;
381 let mut max_precession_angle = 0.0;
382
383 let air_density = calculate_air_density(&self.atmosphere);
385
386 let wind_vector = Vector3::new(
388 self.wind.speed * self.wind.direction.cos(), 0.0,
390 self.wind.speed * self.wind.direction.sin(), );
392
393 while position.x < self.max_range && position.y >= 0.0 && time < 100.0 {
395 let velocity_magnitude = velocity.magnitude();
397 let kinetic_energy = 0.5 * self.inputs.bullet_mass * velocity_magnitude * velocity_magnitude;
398
399 points.push(TrajectoryPoint {
400 time,
401 position: position.clone(),
402 velocity_magnitude,
403 kinetic_energy,
404 });
405
406 if points.len() == 1 || points.len() % 100 == 0 {
408 eprintln!("Trajectory point {}: time={:.3}s, x={:.2}m, y={:.2}m, z={:.2}m, vel={:.1}m/s",
409 points.len(), time, position.x, position.y, position.z, velocity_magnitude);
410 }
411
412 if position.y > max_height {
414 max_height = position.y;
415 }
416
417 if self.inputs.enable_pitch_damping {
419 let temp_c = self.atmosphere.temperature;
420 let temp_k = temp_c + 273.15;
421 let speed_of_sound = (1.4 * 287.05 * temp_k).sqrt();
422 let mach = velocity_magnitude / speed_of_sound;
423
424 if transonic_mach.is_none() && mach < 1.2 && mach > 0.8 {
426 transonic_mach = Some(mach);
427 }
428
429 let bullet_type = if let Some(ref model) = self.inputs.bullet_model {
431 model.as_str()
432 } else {
433 "default"
434 };
435 let coeffs = PitchDampingCoefficients::from_bullet_type(bullet_type);
436 let pitch_damping = calculate_pitch_damping_coefficient(mach, &coeffs);
437
438 if pitch_damping < min_pitch_damping {
440 min_pitch_damping = pitch_damping;
441 }
442 }
443
444 if self.inputs.enable_precession_nutation {
446 if let Some(ref mut state) = angular_state {
447 let velocity_magnitude = velocity.magnitude();
448 let temp_c = self.atmosphere.temperature;
449 let temp_k = temp_c + 273.15;
450 let speed_of_sound = (1.4 * 287.05 * temp_k).sqrt();
451 let mach = velocity_magnitude / speed_of_sound;
452
453 let spin_rate_rad_s = if self.inputs.twist_rate > 0.0 {
455 let velocity_fps = velocity_magnitude * 3.28084;
456 let twist_rate_ft = self.inputs.twist_rate / 12.0;
457 (velocity_fps / twist_rate_ft) * 2.0 * std::f64::consts::PI
458 } else {
459 0.0
460 };
461
462 let params = PrecessionNutationParams {
464 mass_kg: self.inputs.bullet_mass,
465 caliber_m: self.inputs.bullet_diameter,
466 length_m: self.inputs.bullet_length,
467 spin_rate_rad_s,
468 spin_inertia: 6.94e-8, transverse_inertia: 9.13e-7, velocity_mps: velocity_magnitude,
471 air_density_kg_m3: air_density,
472 mach,
473 pitch_damping_coeff: -0.8,
474 nutation_damping_factor: 0.05,
475 };
476
477 *state = calculate_combined_angular_motion(
479 ¶ms,
480 state,
481 time,
482 self.time_step,
483 0.001, );
485
486 if state.yaw_angle.abs() > max_yaw_angle {
488 max_yaw_angle = state.yaw_angle.abs();
489 }
490 if state.precession_angle.abs() > max_precession_angle {
491 max_precession_angle = state.precession_angle.abs();
492 }
493 }
494 }
495
496 let actual_wind = if self.inputs.enable_wind_shear {
498 self.get_wind_at_altitude(position.y)
499 } else {
500 wind_vector.clone()
501 };
502 let velocity_rel = velocity - actual_wind;
503 let velocity_rel_mag = velocity_rel.magnitude();
504 let drag_coefficient = self.calculate_drag_coefficient(velocity_rel_mag);
505
506 let drag_force = 0.5 * air_density * drag_coefficient *
508 self.inputs.bullet_diameter * self.inputs.bullet_diameter *
509 std::f64::consts::PI / 4.0 * velocity_rel_mag * velocity_rel_mag;
510
511 let drag_acceleration = -drag_force / self.inputs.bullet_mass;
513 let acceleration = Vector3::new(
514 drag_acceleration * velocity_rel.x / velocity_rel_mag,
515 drag_acceleration * velocity_rel.y / velocity_rel_mag - 9.80665,
516 drag_acceleration * velocity_rel.z / velocity_rel_mag,
517 );
518
519 velocity += acceleration * self.time_step;
521 position += velocity * self.time_step;
522 time += self.time_step;
523 }
524
525 let last_point = points.last().ok_or("No trajectory points generated")?;
527
528 let sampled_points = if self.inputs.enable_trajectory_sampling {
530 let trajectory_data = TrajectoryData {
531 times: points.iter().map(|p| p.time).collect(),
532 positions: points.iter().map(|p| p.position.clone()).collect(),
533 velocities: points.iter().map(|p| {
534 Vector3::new(0.0, 0.0, p.velocity_magnitude)
536 }).collect(),
537 transonic_distances: Vec::new(), };
539
540 let outputs = TrajectoryOutputs {
541 target_distance_horiz_m: last_point.position.x, target_vertical_height_m: last_point.position.y,
543 time_of_flight_s: last_point.time,
544 max_ord_dist_horiz_m: max_height,
545 };
546
547 let samples = sample_trajectory(&trajectory_data, &outputs, self.inputs.sample_interval, self.inputs.bullet_mass);
549 Some(samples)
550 } else {
551 None
552 };
553
554 Ok(TrajectoryResult {
555 max_range: last_point.position.x, max_height,
557 time_of_flight: last_point.time,
558 impact_velocity: last_point.velocity_magnitude,
559 impact_energy: last_point.kinetic_energy,
560 points,
561 sampled_points,
562 min_pitch_damping: if self.inputs.enable_pitch_damping { Some(min_pitch_damping) } else { None },
563 transonic_mach: transonic_mach,
564 angular_state: angular_state,
565 max_yaw_angle: if self.inputs.enable_precession_nutation { Some(max_yaw_angle) } else { None },
566 max_precession_angle: if self.inputs.enable_precession_nutation { Some(max_precession_angle) } else { None },
567 })
568 }
569
570 fn solve_rk4(&self) -> Result<TrajectoryResult, BallisticsError> {
571 let mut time = 0.0;
573 let mut position = Vector3::new(0.0, self.inputs.sight_height + self.inputs.muzzle_height, 0.0);
574
575 let horizontal_velocity = self.inputs.muzzle_velocity * self.inputs.muzzle_angle.cos();
577 let mut velocity = Vector3::new(
578 horizontal_velocity * self.inputs.azimuth_angle.cos(), self.inputs.muzzle_velocity * self.inputs.muzzle_angle.sin(), horizontal_velocity * self.inputs.azimuth_angle.sin(), );
582
583 let mut points = Vec::new();
584 let mut max_height = position.y;
585 let mut min_pitch_damping = 1.0; let mut transonic_mach = None; let mut angular_state = if self.inputs.enable_precession_nutation {
590 Some(AngularState {
591 pitch_angle: 0.001, yaw_angle: 0.001,
593 pitch_rate: 0.0,
594 yaw_rate: 0.0,
595 precession_angle: 0.0,
596 nutation_phase: 0.0,
597 })
598 } else {
599 None
600 };
601 let mut max_yaw_angle = 0.0;
602 let mut max_precession_angle = 0.0;
603
604 let air_density = calculate_air_density(&self.atmosphere);
606
607 let wind_vector = Vector3::new(
609 self.wind.speed * self.wind.direction.cos(), 0.0,
611 self.wind.speed * self.wind.direction.sin(), );
613
614 while position.x < self.max_range && position.y >= 0.0 && time < 100.0 {
616 let velocity_magnitude = velocity.magnitude();
618 let kinetic_energy = 0.5 * self.inputs.bullet_mass * velocity_magnitude * velocity_magnitude;
619
620 points.push(TrajectoryPoint {
621 time,
622 position: position.clone(),
623 velocity_magnitude,
624 kinetic_energy,
625 });
626
627 if position.y > max_height {
628 max_height = position.y;
629 }
630
631 if self.inputs.enable_pitch_damping {
633 let temp_c = self.atmosphere.temperature;
634 let temp_k = temp_c + 273.15;
635 let speed_of_sound = (1.4 * 287.05 * temp_k).sqrt();
636 let mach = velocity_magnitude / speed_of_sound;
637
638 if transonic_mach.is_none() && mach < 1.2 && mach > 0.8 {
640 transonic_mach = Some(mach);
641 }
642
643 let bullet_type = if let Some(ref model) = self.inputs.bullet_model {
645 model.as_str()
646 } else {
647 "default"
648 };
649 let coeffs = PitchDampingCoefficients::from_bullet_type(bullet_type);
650 let pitch_damping = calculate_pitch_damping_coefficient(mach, &coeffs);
651
652 if pitch_damping < min_pitch_damping {
654 min_pitch_damping = pitch_damping;
655 }
656 }
657
658 if self.inputs.enable_precession_nutation {
660 if let Some(ref mut state) = angular_state {
661 let velocity_magnitude = velocity.magnitude();
662 let temp_c = self.atmosphere.temperature;
663 let temp_k = temp_c + 273.15;
664 let speed_of_sound = (1.4 * 287.05 * temp_k).sqrt();
665 let mach = velocity_magnitude / speed_of_sound;
666
667 let spin_rate_rad_s = if self.inputs.twist_rate > 0.0 {
669 let velocity_fps = velocity_magnitude * 3.28084;
670 let twist_rate_ft = self.inputs.twist_rate / 12.0;
671 (velocity_fps / twist_rate_ft) * 2.0 * std::f64::consts::PI
672 } else {
673 0.0
674 };
675
676 let params = PrecessionNutationParams {
678 mass_kg: self.inputs.bullet_mass,
679 caliber_m: self.inputs.bullet_diameter,
680 length_m: self.inputs.bullet_length,
681 spin_rate_rad_s,
682 spin_inertia: 6.94e-8, transverse_inertia: 9.13e-7, velocity_mps: velocity_magnitude,
685 air_density_kg_m3: air_density,
686 mach,
687 pitch_damping_coeff: -0.8,
688 nutation_damping_factor: 0.05,
689 };
690
691 *state = calculate_combined_angular_motion(
693 ¶ms,
694 state,
695 time,
696 self.time_step,
697 0.001, );
699
700 if state.yaw_angle.abs() > max_yaw_angle {
702 max_yaw_angle = state.yaw_angle.abs();
703 }
704 if state.precession_angle.abs() > max_precession_angle {
705 max_precession_angle = state.precession_angle.abs();
706 }
707 }
708 }
709
710 let dt = self.time_step;
712
713 let acc1 = self.calculate_acceleration(&position, &velocity, air_density, &wind_vector);
715
716 let pos2 = position + velocity * (dt * 0.5);
718 let vel2 = velocity + acc1 * (dt * 0.5);
719 let acc2 = self.calculate_acceleration(&pos2, &vel2, air_density, &wind_vector);
720
721 let pos3 = position + vel2 * (dt * 0.5);
723 let vel3 = velocity + acc2 * (dt * 0.5);
724 let acc3 = self.calculate_acceleration(&pos3, &vel3, air_density, &wind_vector);
725
726 let pos4 = position + vel3 * dt;
728 let vel4 = velocity + acc3 * dt;
729 let acc4 = self.calculate_acceleration(&pos4, &vel4, air_density, &wind_vector);
730
731 position += (velocity + vel2 * 2.0 + vel3 * 2.0 + vel4) * (dt / 6.0);
733 velocity += (acc1 + acc2 * 2.0 + acc3 * 2.0 + acc4) * (dt / 6.0);
734 time += dt;
735 }
736
737 let last_point = points.last().ok_or("No trajectory points generated")?;
739
740 let sampled_points = if self.inputs.enable_trajectory_sampling {
742 let trajectory_data = TrajectoryData {
743 times: points.iter().map(|p| p.time).collect(),
744 positions: points.iter().map(|p| p.position.clone()).collect(),
745 velocities: points.iter().map(|p| {
746 Vector3::new(0.0, 0.0, p.velocity_magnitude)
748 }).collect(),
749 transonic_distances: Vec::new(), };
751
752 let outputs = TrajectoryOutputs {
753 target_distance_horiz_m: last_point.position.x, target_vertical_height_m: last_point.position.y,
755 time_of_flight_s: last_point.time,
756 max_ord_dist_horiz_m: max_height,
757 };
758
759 let samples = sample_trajectory(&trajectory_data, &outputs, self.inputs.sample_interval, self.inputs.bullet_mass);
761 Some(samples)
762 } else {
763 None
764 };
765
766 Ok(TrajectoryResult {
767 max_range: last_point.position.x, max_height,
769 time_of_flight: last_point.time,
770 impact_velocity: last_point.velocity_magnitude,
771 impact_energy: last_point.kinetic_energy,
772 points,
773 sampled_points,
774 min_pitch_damping: if self.inputs.enable_pitch_damping { Some(min_pitch_damping) } else { None },
775 transonic_mach: transonic_mach,
776 angular_state: angular_state,
777 max_yaw_angle: if self.inputs.enable_precession_nutation { Some(max_yaw_angle) } else { None },
778 max_precession_angle: if self.inputs.enable_precession_nutation { Some(max_precession_angle) } else { None },
779 })
780 }
781
782 fn solve_rk45(&self) -> Result<TrajectoryResult, BallisticsError> {
783 let mut time = 0.0;
785 let mut position = Vector3::new(0.0, self.inputs.sight_height + self.inputs.muzzle_height, 0.0);
786
787 let horizontal_velocity = self.inputs.muzzle_velocity * self.inputs.muzzle_angle.cos();
789 let mut velocity = Vector3::new(
790 horizontal_velocity * self.inputs.azimuth_angle.cos(), self.inputs.muzzle_velocity * self.inputs.muzzle_angle.sin(), horizontal_velocity * self.inputs.azimuth_angle.sin(), );
794
795 let mut points = Vec::new();
796 let mut max_height = position.y;
797 let mut dt = 0.001; let tolerance = 1e-6; let safety_factor = 0.9; let max_dt = 0.01; let min_dt = 1e-6; let mut iteration_count = 0;
805 const MAX_ITERATIONS: usize = 100000;
806
807 while position.x < self.max_range && position.y > self.inputs.ground_threshold && time < 100.0 { iteration_count += 1;
809 if iteration_count > MAX_ITERATIONS {
810 break; }
812
813 let velocity_magnitude = velocity.magnitude();
815 let kinetic_energy = 0.5 * self.inputs.bullet_mass * velocity_magnitude.powi(2);
816
817 points.push(TrajectoryPoint {
818 time,
819 position: position.clone(),
820 velocity_magnitude,
821 kinetic_energy,
822 });
823
824 if position.y > max_height {
825 max_height = position.y;
826 }
827
828 let air_density = calculate_air_density(&self.atmosphere);
830 let wind_vector = Vector3::new(
831 self.wind.speed * self.wind.direction.cos(), 0.0,
833 self.wind.speed * self.wind.direction.sin(), );
835
836 let (new_pos, new_vel, new_dt) = self.rk45_step(
838 &position,
839 &velocity,
840 dt,
841 air_density,
842 &wind_vector,
843 tolerance,
844 );
845
846 dt = (safety_factor * new_dt).clamp(min_dt, max_dt);
848
849 position = new_pos;
851 velocity = new_vel;
852 time += dt;
853 }
854
855 if points.is_empty() {
857 return Err(BallisticsError::from("No trajectory points calculated"));
858 }
859
860 let last_point = points.last().unwrap();
861
862 Ok(TrajectoryResult {
863 max_range: last_point.position.x, max_height,
865 time_of_flight: last_point.time,
866 impact_velocity: last_point.velocity_magnitude,
867 impact_energy: last_point.kinetic_energy,
868 points,
869 sampled_points: None, min_pitch_damping: None,
871 transonic_mach: None,
872 angular_state: None,
873 max_yaw_angle: None,
874 max_precession_angle: None,
875 })
876 }
877
878 fn rk45_step(
879 &self,
880 position: &Vector3<f64>,
881 velocity: &Vector3<f64>,
882 dt: f64,
883 air_density: f64,
884 wind_vector: &Vector3<f64>,
885 tolerance: f64,
886 ) -> (Vector3<f64>, Vector3<f64>, f64) {
887 const A21: f64 = 1.0 / 5.0;
889 const A31: f64 = 3.0 / 40.0;
890 const A32: f64 = 9.0 / 40.0;
891 const A41: f64 = 44.0 / 45.0;
892 const A42: f64 = -56.0 / 15.0;
893 const A43: f64 = 32.0 / 9.0;
894 const A51: f64 = 19372.0 / 6561.0;
895 const A52: f64 = -25360.0 / 2187.0;
896 const A53: f64 = 64448.0 / 6561.0;
897 const A54: f64 = -212.0 / 729.0;
898 const A61: f64 = 9017.0 / 3168.0;
899 const A62: f64 = -355.0 / 33.0;
900 const A63: f64 = 46732.0 / 5247.0;
901 const A64: f64 = 49.0 / 176.0;
902 const A65: f64 = -5103.0 / 18656.0;
903 const A71: f64 = 35.0 / 384.0;
904 const A73: f64 = 500.0 / 1113.0;
905 const A74: f64 = 125.0 / 192.0;
906 const A75: f64 = -2187.0 / 6784.0;
907 const A76: f64 = 11.0 / 84.0;
908
909 const B1: f64 = 35.0 / 384.0;
911 const B3: f64 = 500.0 / 1113.0;
912 const B4: f64 = 125.0 / 192.0;
913 const B5: f64 = -2187.0 / 6784.0;
914 const B6: f64 = 11.0 / 84.0;
915
916 const B1_ERR: f64 = 5179.0 / 57600.0;
918 const B3_ERR: f64 = 7571.0 / 16695.0;
919 const B4_ERR: f64 = 393.0 / 640.0;
920 const B5_ERR: f64 = -92097.0 / 339200.0;
921 const B6_ERR: f64 = 187.0 / 2100.0;
922 const B7_ERR: f64 = 1.0 / 40.0;
923
924 let k1_v = self.calculate_acceleration(position, velocity, air_density, wind_vector);
926 let k1_p = velocity.clone();
927
928 let p2 = position + dt * A21 * k1_p;
929 let v2 = velocity + dt * A21 * k1_v;
930 let k2_v = self.calculate_acceleration(&p2, &v2, air_density, wind_vector);
931 let k2_p = v2;
932
933 let p3 = position + dt * (A31 * k1_p + A32 * k2_p);
934 let v3 = velocity + dt * (A31 * k1_v + A32 * k2_v);
935 let k3_v = self.calculate_acceleration(&p3, &v3, air_density, wind_vector);
936 let k3_p = v3;
937
938 let p4 = position + dt * (A41 * k1_p + A42 * k2_p + A43 * k3_p);
939 let v4 = velocity + dt * (A41 * k1_v + A42 * k2_v + A43 * k3_v);
940 let k4_v = self.calculate_acceleration(&p4, &v4, air_density, wind_vector);
941 let k4_p = v4;
942
943 let p5 = position + dt * (A51 * k1_p + A52 * k2_p + A53 * k3_p + A54 * k4_p);
944 let v5 = velocity + dt * (A51 * k1_v + A52 * k2_v + A53 * k3_v + A54 * k4_v);
945 let k5_v = self.calculate_acceleration(&p5, &v5, air_density, wind_vector);
946 let k5_p = v5;
947
948 let p6 = position + dt * (A61 * k1_p + A62 * k2_p + A63 * k3_p + A64 * k4_p + A65 * k5_p);
949 let v6 = velocity + dt * (A61 * k1_v + A62 * k2_v + A63 * k3_v + A64 * k4_v + A65 * k5_v);
950 let k6_v = self.calculate_acceleration(&p6, &v6, air_density, wind_vector);
951 let k6_p = v6;
952
953 let p7 = position + dt * (A71 * k1_p + A73 * k3_p + A74 * k4_p + A75 * k5_p + A76 * k6_p);
954 let v7 = velocity + dt * (A71 * k1_v + A73 * k3_v + A74 * k4_v + A75 * k5_v + A76 * k6_v);
955 let k7_v = self.calculate_acceleration(&p7, &v7, air_density, wind_vector);
956 let k7_p = v7;
957
958 let new_pos = position + dt * (B1 * k1_p + B3 * k3_p + B4 * k4_p + B5 * k5_p + B6 * k6_p);
960 let new_vel = velocity + dt * (B1 * k1_v + B3 * k3_v + B4 * k4_v + B5 * k5_v + B6 * k6_v);
961
962 let pos_err = position + dt * (B1_ERR * k1_p + B3_ERR * k3_p + B4_ERR * k4_p + B5_ERR * k5_p + B6_ERR * k6_p + B7_ERR * k7_p);
964 let vel_err = velocity + dt * (B1_ERR * k1_v + B3_ERR * k3_v + B4_ERR * k4_v + B5_ERR * k5_v + B6_ERR * k6_v + B7_ERR * k7_v);
965
966 let pos_error = (new_pos - pos_err).magnitude();
968 let vel_error = (new_vel - vel_err).magnitude();
969 let error = (pos_error + vel_error) / (1.0 + position.magnitude() + velocity.magnitude());
970
971 let dt_new = if error < tolerance {
973 dt * (tolerance / error).powf(0.2).min(2.0)
974 } else {
975 dt * (tolerance / error).powf(0.25).max(0.1)
976 };
977
978 (new_pos, new_vel, dt_new)
979 }
980
981 fn calculate_acceleration(&self, position: &Vector3<f64>, velocity: &Vector3<f64>, air_density: f64, wind_vector: &Vector3<f64>) -> Vector3<f64> {
982 let actual_wind = if self.inputs.enable_wind_shear {
984 self.get_wind_at_altitude(position.y)
985 } else {
986 wind_vector.clone()
987 };
988
989 let relative_velocity = velocity - &actual_wind;
990 let velocity_magnitude = relative_velocity.magnitude();
991
992 if velocity_magnitude < 0.001 {
993 return Vector3::new(0.0, -9.81, 0.0);
994 }
995
996 let cd = self.calculate_drag_coefficient(velocity_magnitude);
998
999 let effective_bc = if let Some(ref cluster_bc) = self.cluster_bc {
1001 let velocity_fps = velocity_magnitude * 3.28084;
1003 cluster_bc.apply_correction(
1004 self.inputs.bc_value,
1005 self.inputs.caliber_inches * 0.0254, self.inputs.weight_grains,
1007 velocity_fps
1008 )
1009 } else {
1010 self.inputs.bc_value
1011 };
1012
1013 let velocity_fps = velocity_magnitude * 3.28084; let cd_to_retard = 0.000683 * 0.30; let standard_factor = cd * cd_to_retard;
1019 let density_scale = air_density / 1.225; let a_drag_ft_s2 = (velocity_fps * velocity_fps) * standard_factor * density_scale / effective_bc;
1023 let a_drag_m_s2 = a_drag_ft_s2 * 0.3048; let drag_acceleration = -a_drag_m_s2 * (relative_velocity / velocity_magnitude);
1027
1028 drag_acceleration + Vector3::new(0.0, -9.81, 0.0)
1030 }
1031
1032 fn calculate_drag_coefficient(&self, velocity: f64) -> f64 {
1033 let temp_c = self.atmosphere.temperature;
1035 let temp_k = temp_c + 273.15;
1036 let gamma = 1.4; let r_specific = 287.05; let speed_of_sound = (gamma * r_specific * temp_k).sqrt();
1039 let mach = velocity / speed_of_sound;
1040
1041 let base_cd = crate::drag::get_drag_coefficient(mach, &self.inputs.bc_type);
1043
1044 let projectile_shape = if let Some(ref model) = self.inputs.bullet_model {
1046 if model.to_lowercase().contains("boat") || model.to_lowercase().contains("bt") {
1048 ProjectileShape::BoatTail
1049 } else if model.to_lowercase().contains("round") || model.to_lowercase().contains("rn") {
1050 ProjectileShape::RoundNose
1051 } else if model.to_lowercase().contains("flat") || model.to_lowercase().contains("fb") {
1052 ProjectileShape::FlatBase
1053 } else {
1054 get_projectile_shape(
1056 self.inputs.bullet_diameter,
1057 self.inputs.bullet_mass / 0.00006479891, &self.inputs.bc_type.to_string()
1059 )
1060 }
1061 } else {
1062 get_projectile_shape(
1064 self.inputs.bullet_diameter,
1065 self.inputs.bullet_mass / 0.00006479891, &self.inputs.bc_type.to_string()
1067 )
1068 };
1069
1070 let include_wave_drag = self.inputs.enable_advanced_effects;
1073 transonic_correction(mach, base_cd, projectile_shape, include_wave_drag)
1074 }
1075}
1076
1077#[derive(Debug, Clone)]
1079pub struct MonteCarloParams {
1080 pub num_simulations: usize,
1081 pub velocity_std_dev: f64,
1082 pub angle_std_dev: f64,
1083 pub bc_std_dev: f64,
1084 pub wind_speed_std_dev: f64,
1085 pub target_distance: Option<f64>,
1086 pub base_wind_speed: f64,
1087 pub base_wind_direction: f64,
1088 pub azimuth_std_dev: f64, }
1090
1091impl Default for MonteCarloParams {
1092 fn default() -> Self {
1093 Self {
1094 num_simulations: 1000,
1095 velocity_std_dev: 1.0,
1096 angle_std_dev: 0.001,
1097 bc_std_dev: 0.01,
1098 wind_speed_std_dev: 1.0,
1099 target_distance: None,
1100 base_wind_speed: 0.0,
1101 base_wind_direction: 0.0,
1102 azimuth_std_dev: 0.001, }
1104 }
1105}
1106
1107#[derive(Debug, Clone)]
1109pub struct MonteCarloResults {
1110 pub ranges: Vec<f64>,
1111 pub impact_velocities: Vec<f64>,
1112 pub impact_positions: Vec<Vector3<f64>>,
1113}
1114
1115pub fn run_monte_carlo(
1117 base_inputs: BallisticInputs,
1118 params: MonteCarloParams,
1119) -> Result<MonteCarloResults, BallisticsError> {
1120 let base_wind = WindConditions {
1121 speed: params.base_wind_speed,
1122 direction: params.base_wind_direction,
1123 };
1124 run_monte_carlo_with_wind(base_inputs, base_wind, params)
1125}
1126
1127pub fn run_monte_carlo_with_wind(
1129 base_inputs: BallisticInputs,
1130 base_wind: WindConditions,
1131 params: MonteCarloParams,
1132) -> Result<MonteCarloResults, BallisticsError> {
1133 use rand::thread_rng;
1134 use rand_distr::{Distribution, Normal};
1135
1136 let mut rng = thread_rng();
1137 let mut ranges = Vec::new();
1138 let mut impact_velocities = Vec::new();
1139 let mut impact_positions = Vec::new();
1140
1141 let baseline_solver = TrajectorySolver::new(base_inputs.clone(), base_wind.clone(), Default::default());
1143 let baseline_result = baseline_solver.solve()?;
1144 let baseline_impact = baseline_result.points.last()
1145 .ok_or("No baseline trajectory points")?
1146 .position.clone();
1147
1148 let velocity_dist = Normal::new(base_inputs.muzzle_velocity, params.velocity_std_dev)
1150 .map_err(|e| format!("Invalid velocity distribution: {}", e))?;
1151 let angle_dist = Normal::new(base_inputs.muzzle_angle, params.angle_std_dev)
1152 .map_err(|e| format!("Invalid angle distribution: {}", e))?;
1153 let bc_dist = Normal::new(base_inputs.bc_value, params.bc_std_dev)
1154 .map_err(|e| format!("Invalid BC distribution: {}", e))?;
1155 let wind_speed_dist = Normal::new(base_wind.speed, params.wind_speed_std_dev)
1156 .map_err(|e| format!("Invalid wind speed distribution: {}", e))?;
1157 let wind_dir_dist = Normal::new(base_wind.direction, params.wind_speed_std_dev * 0.1) .map_err(|e| format!("Invalid wind direction distribution: {}", e))?;
1159 let azimuth_dist = Normal::new(base_inputs.azimuth_angle, params.azimuth_std_dev)
1160 .map_err(|e| format!("Invalid azimuth distribution: {}", e))?;
1161
1162 let distance = baseline_impact.z; let pointing_error_dist = Normal::new(0.0, params.angle_std_dev * distance)
1165 .map_err(|e| format!("Invalid pointing distribution: {}", e))?;
1166
1167 for _ in 0..params.num_simulations {
1168 let mut inputs = base_inputs.clone();
1170 inputs.muzzle_velocity = velocity_dist.sample(&mut rng).max(0.0);
1171 inputs.muzzle_angle = angle_dist.sample(&mut rng);
1172 inputs.bc_value = bc_dist.sample(&mut rng).max(0.01);
1173 inputs.azimuth_angle = azimuth_dist.sample(&mut rng); let wind = WindConditions {
1177 speed: wind_speed_dist.sample(&mut rng).abs(),
1178 direction: wind_dir_dist.sample(&mut rng),
1179 };
1180
1181 let solver = TrajectorySolver::new(inputs, wind, Default::default());
1183 match solver.solve() {
1184 Ok(result) => {
1185 ranges.push(result.max_range);
1186 impact_velocities.push(result.impact_velocity);
1187 if let Some(last_point) = result.points.last() {
1188 let mut deviation = Vector3::new(
1190 last_point.position.x - baseline_impact.x, last_point.position.y - baseline_impact.y, last_point.position.z - baseline_impact.z, );
1194
1195 let pointing_error_y = pointing_error_dist.sample(&mut rng);
1198 deviation.y += pointing_error_y;
1199
1200 impact_positions.push(deviation);
1201 }
1202 },
1203 Err(_) => {
1204 continue;
1206 }
1207 }
1208 }
1209
1210 if ranges.is_empty() {
1211 return Err("No successful simulations".into());
1212 }
1213
1214 Ok(MonteCarloResults {
1215 ranges,
1216 impact_velocities,
1217 impact_positions,
1218 })
1219}
1220
1221pub fn calculate_zero_angle(
1223 inputs: BallisticInputs,
1224 target_distance: f64,
1225 target_height: f64,
1226) -> Result<f64, BallisticsError> {
1227 calculate_zero_angle_with_conditions(
1228 inputs,
1229 target_distance,
1230 target_height,
1231 WindConditions::default(),
1232 AtmosphericConditions::default(),
1233 )
1234}
1235
1236pub fn calculate_zero_angle_with_conditions(
1237 inputs: BallisticInputs,
1238 target_distance: f64,
1239 target_height: f64,
1240 wind: WindConditions,
1241 atmosphere: AtmosphericConditions,
1242) -> Result<f64, BallisticsError> {
1243 let get_height_at_angle = |angle: f64| -> Result<Option<f64>, BallisticsError> {
1245 let mut test_inputs = inputs.clone();
1246 test_inputs.muzzle_angle = angle;
1247
1248 let mut solver = TrajectorySolver::new(test_inputs, wind.clone(), atmosphere.clone());
1249 solver.set_max_range(target_distance * 2.0);
1250 solver.set_time_step(0.001);
1251 let result = solver.solve()?;
1252
1253 for i in 0..result.points.len() {
1254 if result.points[i].position.x >= target_distance {
1255 if i > 0 {
1256 let p1 = &result.points[i - 1];
1257 let p2 = &result.points[i];
1258 let t = (target_distance - p1.position.x) / (p2.position.x - p1.position.x);
1259 return Ok(Some(p1.position.y + t * (p2.position.y - p1.position.y)));
1260 } else {
1261 return Ok(Some(result.points[i].position.y));
1262 }
1263 }
1264 }
1265 Ok(None)
1266 };
1267
1268 let mut low_angle = 0.0; let mut high_angle = 0.2; let tolerance = 0.00001; let max_iterations = 50;
1274
1275 let low_height = get_height_at_angle(low_angle)?;
1278 let high_height = get_height_at_angle(high_angle)?;
1279
1280 match (low_height, high_height) {
1281 (Some(lh), Some(hh)) => {
1282 let low_error = lh - target_height;
1283 let high_error = hh - target_height;
1284
1285 if low_error > 0.0 && high_error > 0.0 {
1288 } else if low_error < 0.0 && high_error < 0.0 {
1293 let mut expanded = false;
1296 for multiplier in [2.0, 3.0, 4.0] {
1297 let new_high = (high_angle * multiplier).min(0.785);
1298 if let Ok(Some(h)) = get_height_at_angle(new_high) {
1299 if h - target_height > 0.0 {
1300 high_angle = new_high;
1301 expanded = true;
1302 break;
1303 }
1304 }
1305 if new_high >= 0.785 {
1306 break;
1307 }
1308 }
1309 if !expanded {
1310 return Err("Cannot find zero angle: target beyond effective range even at maximum angle".into());
1311 }
1312 }
1313 },
1315 (None, Some(_hh)) => {
1316 },
1319 (Some(_lh), None) => {
1320 return Err("Cannot find zero angle: high angle trajectory doesn't reach target distance".into());
1322 },
1323 (None, None) => {
1324 return Err("Cannot find zero angle: trajectory cannot reach target distance at any angle".into());
1326 }
1327 }
1328
1329 for _iteration in 0..max_iterations {
1330 let mid_angle = (low_angle + high_angle) / 2.0;
1331
1332 let mut test_inputs = inputs.clone();
1333 test_inputs.muzzle_angle = mid_angle;
1334
1335 let mut solver = TrajectorySolver::new(test_inputs, wind.clone(), atmosphere.clone());
1336 solver.set_max_range(target_distance * 2.0);
1338 solver.set_time_step(0.001);
1339 let result = solver.solve()?;
1340
1341 let mut height_at_target = None;
1343 for i in 0..result.points.len() {
1344 if result.points[i].position.x >= target_distance {
1345 if i > 0 {
1346 let p1 = &result.points[i - 1];
1348 let p2 = &result.points[i];
1349 let t = (target_distance - p1.position.x) / (p2.position.x - p1.position.x);
1350 height_at_target = Some(p1.position.y + t * (p2.position.y - p1.position.y));
1351 } else {
1352 height_at_target = Some(result.points[i].position.y);
1353 }
1354 break;
1355 }
1356 }
1357
1358 match height_at_target {
1359 Some(height) => {
1360 let error = height - target_height;
1361 if error.abs() < 0.001 {
1364 return Ok(mid_angle);
1365 }
1366
1367 if (high_angle - low_angle).abs() < tolerance {
1371 if error.abs() < 0.01 {
1372 return Ok(mid_angle);
1374 }
1375 return Ok(mid_angle);
1378 }
1379
1380 if error > 0.0 {
1381 high_angle = mid_angle;
1382 } else {
1383 low_angle = mid_angle;
1384 }
1385 },
1386 None => {
1387 low_angle = mid_angle;
1389
1390 if (high_angle - low_angle).abs() < tolerance {
1392 return Err("Trajectory cannot reach target distance - angle converged without valid solution".into());
1393 }
1394 }
1395 }
1396 }
1397
1398 Err("Failed to find zero angle".into())
1399}
1400
1401pub fn estimate_bc_from_trajectory(
1403 velocity: f64,
1404 mass: f64,
1405 diameter: f64,
1406 points: &[(f64, f64)], ) -> Result<f64, BallisticsError> {
1408 let mut best_bc = 0.5;
1410 let mut best_error = f64::MAX;
1411 let mut found_valid = false;
1412
1413 for bc in (100..1000).step_by(10) {
1415 let bc_value = bc as f64 / 1000.0;
1416
1417 let inputs = BallisticInputs {
1418 muzzle_velocity: velocity,
1419 bc_value,
1420 bullet_mass: mass,
1421 bullet_diameter: diameter,
1422 ..Default::default()
1423 };
1424
1425 let mut solver = TrajectorySolver::new(inputs, Default::default(), Default::default());
1426 solver.set_max_range(points.last().map(|(d, _)| *d * 1.5).unwrap_or(1000.0));
1428
1429 let result = match solver.solve() {
1430 Ok(r) => r,
1431 Err(_) => continue, };
1433
1434 let mut total_error = 0.0;
1436 for (target_dist, target_drop) in points {
1437 let mut calculated_drop = None;
1439 for i in 0..result.points.len() {
1440 if result.points[i].position.z >= *target_dist {
1441 if i > 0 {
1442 let p1 = &result.points[i - 1];
1444 let p2 = &result.points[i];
1445 let t = (target_dist - p1.position.z) / (p2.position.z - p1.position.z);
1446 calculated_drop = Some(-(p1.position.y + t * (p2.position.y - p1.position.y)));
1447 } else {
1448 calculated_drop = Some(-result.points[i].position.y);
1449 }
1450 break;
1451 }
1452 }
1453
1454 if let Some(drop) = calculated_drop {
1455 let error = (drop - target_drop).abs();
1456 total_error += error * error;
1457 }
1458 }
1459
1460 if total_error < best_error {
1461 best_error = total_error;
1462 best_bc = bc_value;
1463 found_valid = true;
1464 }
1465 }
1466
1467 if !found_valid {
1468 return Err(BallisticsError::from("Unable to estimate BC from provided data. Check that drop values are in correct units.".to_string()));
1469 }
1470
1471 Ok(best_bc)
1472}
1473
1474fn calculate_air_density(atmosphere: &AtmosphericConditions) -> f64 {
1476 let r_specific = 287.058; let temperature_k = atmosphere.temperature + 273.15;
1480
1481 let pressure_pa = atmosphere.pressure * 100.0;
1483
1484 let density = pressure_pa / (r_specific * temperature_k);
1486
1487 let altitude_factor = (-atmosphere.altitude / 8000.0).exp();
1489
1490 density * altitude_factor
1491}
1492
1493use rand;
1495use rand_distr;