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)]
56pub struct BallisticInputs {
57 pub muzzle_velocity: f64, pub launch_angle: f64, pub ballistic_coefficient: f64,
61 pub mass: f64, pub diameter: f64, pub drag_model: DragModel,
64 pub sight_height: f64, pub muzzle_height: f64, pub altitude: f64, pub bc_type: DragModel, pub bc_value: f64, pub caliber_inches: f64, pub weight_grains: f64, pub bullet_diameter: f64, pub bullet_mass: f64, pub bullet_length: f64, pub muzzle_angle: f64, pub target_distance: f64, pub azimuth_angle: f64, pub use_rk4: bool, pub use_adaptive_rk45: bool, pub temperature: f64, pub twist_rate: f64, pub is_twist_right: bool, pub shooting_angle: f64, pub latitude: Option<f64>, pub ground_threshold: f64, pub target_height: f64, pub enable_advanced_effects: bool,
91 pub use_powder_sensitivity: bool,
92 pub powder_temp_sensitivity: f64,
93 pub powder_temp: f64, pub tipoff_yaw: f64, pub tipoff_decay_distance: f64, pub use_bc_segments: bool,
97 pub bc_segments: Option<Vec<(f64, f64)>>, pub bc_segments_data: Option<Vec<crate::BCSegmentData>>, 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, pub enable_pitch_damping: bool,
106 pub enable_precession_nutation: bool,
107 pub use_cluster_bc: bool, 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 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, altitude: 0.0,
136 bc_type: drag_model,
137 bc_value: bc,
138 caliber_inches: diameter_m / 0.0254, weight_grains: mass_kg / 0.00006479891, bullet_diameter: diameter_m,
141 bullet_mass: mass_kg,
142 bullet_length: diameter_m * 4.0, muzzle_angle: launch_angle_rad,
144 target_distance: 100.0,
145 azimuth_angle: 0.0,
146 use_rk4: true, use_adaptive_rk45: true, temperature: 15.0,
149 twist_rate: 12.0, is_twist_right: true,
151 shooting_angle: 0.0,
152 latitude: None,
153 ground_threshold: -0.001, target_height: 0.0, 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, enable_pitch_damping: false,
173 enable_precession_nutation: false,
174 use_cluster_bc: false, bc_type_str: None,
178 bullet_model: None,
179 bullet_id: None,
180 }
181 }
182}
183
184#[derive(Debug, Clone)]
186pub struct WindConditions {
187 pub speed: f64, pub direction: f64, }
190
191impl Default for WindConditions {
192 fn default() -> Self {
193 Self {
194 speed: 0.0,
195 direction: 0.0,
196 }
197 }
198}
199
200#[derive(Debug, Clone)]
202pub struct AtmosphericConditions {
203 pub temperature: f64, pub pressure: f64, pub humidity: f64, pub altitude: f64, }
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#[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#[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>>, 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>, }
245
246pub 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 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 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 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 },
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, roughness_length: 0.03, power_exponent: 1.0 / 7.0, 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 let mut time = 0.0;
332 let mut position = Vector3::new(0.0, self.inputs.sight_height + self.inputs.muzzle_height, 0.0);
333 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(), self.inputs.muzzle_velocity * self.inputs.launch_angle.sin(), horizontal_velocity * self.inputs.azimuth_angle.cos(), );
340
341 let mut points = Vec::new();
342 let mut max_height = position.y;
343 let mut min_pitch_damping = 1.0; let mut transonic_mach = None; let mut angular_state = if self.inputs.enable_precession_nutation {
348 Some(AngularState {
349 pitch_angle: 0.001, 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 let air_density = calculate_air_density(&self.atmosphere);
364
365 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 while position.z < self.max_range && position.y >= 0.0 && time < 100.0 {
374 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 if position.y > max_height {
387 max_height = position.y;
388 }
389
390 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 if transonic_mach.is_none() && mach < 1.2 && mach > 0.8 {
399 transonic_mach = Some(mach);
400 }
401
402 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 if pitch_damping < min_pitch_damping {
413 min_pitch_damping = pitch_damping;
414 }
415 }
416
417 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 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 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, transverse_inertia: 9.13e-7, 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 *state = calculate_combined_angular_motion(
452 ¶ms,
453 state,
454 time,
455 self.time_step,
456 0.001, );
458
459 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 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 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 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 velocity += acceleration * self.time_step;
494 position += velocity * self.time_step;
495 time += self.time_step;
496 }
497
498 let last_point = points.last().ok_or("No trajectory points generated")?;
500
501 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 Vector3::new(0.0, 0.0, p.velocity_magnitude)
509 }).collect(),
510 transonic_distances: Vec::new(), };
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 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 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 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; let mut transonic_mach = None; let mut angular_state = if self.inputs.enable_precession_nutation {
563 Some(AngularState {
564 pitch_angle: 0.001, 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 let air_density = calculate_air_density(&self.atmosphere);
579
580 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 while position.z < self.max_range && position.y >= 0.0 && time < 100.0 {
589 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 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 if transonic_mach.is_none() && mach < 1.2 && mach > 0.8 {
613 transonic_mach = Some(mach);
614 }
615
616 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 if pitch_damping < min_pitch_damping {
627 min_pitch_damping = pitch_damping;
628 }
629 }
630
631 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 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 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, transverse_inertia: 9.13e-7, 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 *state = calculate_combined_angular_motion(
666 ¶ms,
667 state,
668 time,
669 self.time_step,
670 0.001, );
672
673 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 let dt = self.time_step;
685
686 let acc1 = self.calculate_acceleration(&position, &velocity, air_density, &wind_vector);
688
689 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 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 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 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 let last_point = points.last().ok_or("No trajectory points generated")?;
712
713 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 Vector3::new(0.0, 0.0, p.velocity_magnitude)
721 }).collect(),
722 transonic_distances: Vec::new(), };
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 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 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 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; let tolerance = 1e-6; let safety_factor = 0.9; let max_dt = 0.01; let min_dt = 1e-6; 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; }
785
786 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 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 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 dt = (safety_factor * new_dt).clamp(min_dt, max_dt);
821
822 position = new_pos;
824 velocity = new_vel;
825 time += dt;
826 }
827
828 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, 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 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 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 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 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 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 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 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 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 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 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 let effective_bc = if let Some(ref cluster_bc) = self.cluster_bc {
975 let velocity_fps = velocity_magnitude * 3.28084;
977 cluster_bc.apply_correction(
978 self.inputs.ballistic_coefficient,
979 self.inputs.caliber_inches * 0.0254, 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 let drag_force = -relative_velocity.normalize() * drag_magnitude;
991
992 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 let temp_c = self.atmosphere.temperature;
1002 let temp_k = temp_c + 273.15;
1003 let gamma = 1.4; let r_specific = 287.05; let speed_of_sound = (gamma * r_specific * temp_k).sqrt();
1006 let mach = velocity / speed_of_sound;
1007
1008 let base_cd = match self.inputs.drag_model {
1010 DragModel::G1 => 0.5,
1011 DragModel::G7 => 0.4,
1012 _ => 0.45,
1013 };
1014
1015 let projectile_shape = if let Some(ref model) = self.inputs.bullet_model {
1017 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 get_projectile_shape(
1027 self.inputs.diameter,
1028 self.inputs.mass / 0.00006479891, &self.inputs.drag_model.to_string()
1030 )
1031 }
1032 } else {
1033 get_projectile_shape(
1035 self.inputs.diameter,
1036 self.inputs.mass / 0.00006479891, &self.inputs.drag_model.to_string()
1038 )
1039 };
1040
1041 let include_wave_drag = self.inputs.enable_advanced_effects;
1044 transonic_correction(mach, base_cd, projectile_shape, include_wave_drag)
1045 }
1046}
1047
1048#[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, }
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, }
1075 }
1076}
1077
1078#[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
1086pub 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
1098pub 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 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 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) .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 let distance = baseline_impact.z; 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 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); let wind = WindConditions {
1148 speed: wind_speed_dist.sample(&mut rng).abs(),
1149 direction: wind_dir_dist.sample(&mut rng),
1150 };
1151
1152 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 let mut deviation = Vector3::new(
1161 last_point.position.x - baseline_impact.x, last_point.position.y - baseline_impact.y, last_point.position.z - baseline_impact.z, );
1165
1166 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 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
1192pub 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 let mut low_angle = -0.2; let mut high_angle = 0.2; let tolerance = 0.00001; 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 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 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 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 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
1282pub fn estimate_bc_from_trajectory(
1284 velocity: f64,
1285 mass: f64,
1286 diameter: f64,
1287 points: &[(f64, f64)], ) -> Result<f64, BallisticsError> {
1289 let mut best_bc = 0.5;
1291 let mut best_error = f64::MAX;
1292 let mut found_valid = false;
1293
1294 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 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, };
1314
1315 let mut total_error = 0.0;
1317 for (target_dist, target_drop) in points {
1318 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 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
1355fn calculate_air_density(atmosphere: &AtmosphericConditions) -> f64 {
1357 let r_specific = 287.058; let temperature_k = atmosphere.temperature + 273.15;
1361
1362 let pressure_pa = atmosphere.pressure * 100.0;
1364
1365 let density = pressure_pa / (r_specific * temperature_k);
1367
1368 let altitude_factor = (-atmosphere.altitude / 8000.0).exp();
1370
1371 density * altitude_factor
1372}
1373
1374use rand;
1376use rand_distr;