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#[cfg_attr(feature = "python", pyo3::pyclass)]
58#[derive(Debug, Clone)]
59pub struct BallisticInputs {
60 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,
105 pub use_powder_sensitivity: bool,
106 pub powder_temp_sensitivity: f64,
107 pub powder_temp: f64, pub tipoff_yaw: f64, pub tipoff_decay_distance: f64, pub use_bc_segments: bool,
111 pub bc_segments: Option<Vec<(f64, f64)>>, pub bc_segments_data: Option<Vec<crate::BCSegmentData>>, pub use_enhanced_spin_drift: bool,
114 pub use_form_factor: bool,
115 pub enable_wind_shear: bool,
116 pub wind_shear_model: String,
117 pub enable_trajectory_sampling: bool,
118 pub sample_interval: f64, pub enable_pitch_damping: bool,
120 pub enable_precession_nutation: bool,
121 pub use_cluster_bc: bool, pub custom_drag_table: Option<crate::drag::DragTable>,
125
126 pub bc_type_str: Option<String>,
128}
129
130impl Default for BallisticInputs {
131 fn default() -> Self {
132 let mass_kg = 0.01;
133 let diameter_m = 0.00762;
134 let bc = 0.5;
135 let muzzle_angle_rad = 0.0;
136 let bc_type = DragModel::G1;
137
138 Self {
139 bc_value: bc,
141 bc_type,
142 bullet_mass: mass_kg,
143 muzzle_velocity: 800.0,
144 bullet_diameter: diameter_m,
145 bullet_length: diameter_m * 4.0, muzzle_angle: muzzle_angle_rad,
149 target_distance: 100.0,
150 azimuth_angle: 0.0,
151 shooting_angle: 0.0,
152 sight_height: 0.05,
153 muzzle_height: 1.5, target_height: 0.0, ground_threshold: -0.001, altitude: 0.0,
159 temperature: 15.0,
160 pressure: 1013.25, humidity: 0.5, latitude: None,
163
164 wind_speed: 0.0,
166 wind_angle: 0.0,
167
168 twist_rate: 12.0, is_twist_right: true,
171 caliber_inches: diameter_m / 0.0254, weight_grains: mass_kg / 0.00006479891, manufacturer: None,
174 bullet_model: None,
175 bullet_id: None,
176 bullet_cluster: None,
177
178 use_rk4: true, use_adaptive_rk45: true, enable_advanced_effects: false,
184 use_powder_sensitivity: false,
185 powder_temp_sensitivity: 0.0,
186 powder_temp: 15.0,
187 tipoff_yaw: 0.0,
188 tipoff_decay_distance: 50.0,
189 use_bc_segments: false,
190 bc_segments: None,
191 bc_segments_data: None,
192 use_enhanced_spin_drift: false,
193 use_form_factor: false,
194 enable_wind_shear: false,
195 wind_shear_model: "none".to_string(),
196 enable_trajectory_sampling: false,
197 sample_interval: 10.0, enable_pitch_damping: false,
199 enable_precession_nutation: false,
200 use_cluster_bc: false, custom_drag_table: None,
204
205 bc_type_str: None,
207 }
208 }
209}
210
211#[derive(Debug, Clone)]
213pub struct WindConditions {
214 pub speed: f64, pub direction: f64, }
217
218impl Default for WindConditions {
219 fn default() -> Self {
220 Self {
221 speed: 0.0,
222 direction: 0.0,
223 }
224 }
225}
226
227#[derive(Debug, Clone)]
229pub struct AtmosphericConditions {
230 pub temperature: f64, pub pressure: f64, pub humidity: f64, pub altitude: f64, }
235
236impl Default for AtmosphericConditions {
237 fn default() -> Self {
238 Self {
239 temperature: 15.0,
240 pressure: 1013.25,
241 humidity: 50.0,
242 altitude: 0.0,
243 }
244 }
245}
246
247#[derive(Debug, Clone)]
249pub struct TrajectoryPoint {
250 pub time: f64,
251 pub position: Vector3<f64>,
252 pub velocity_magnitude: f64,
253 pub kinetic_energy: f64,
254}
255
256#[derive(Debug, Clone)]
258pub struct TrajectoryResult {
259 pub max_range: f64,
260 pub max_height: f64,
261 pub time_of_flight: f64,
262 pub impact_velocity: f64,
263 pub impact_energy: f64,
264 pub points: Vec<TrajectoryPoint>,
265 pub sampled_points: Option<Vec<TrajectorySample>>, 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>, }
272
273pub struct TrajectorySolver {
275 inputs: BallisticInputs,
276 wind: WindConditions,
277 atmosphere: AtmosphericConditions,
278 max_range: f64,
279 time_step: f64,
280 cluster_bc: Option<ClusterBCDegradation>,
281}
282
283impl TrajectorySolver {
284 pub fn new(mut inputs: BallisticInputs, wind: WindConditions, atmosphere: AtmosphericConditions) -> Self {
285 inputs.bc_type = inputs.bc_type;
287 inputs.bc_value = inputs.bc_value;
288 inputs.bullet_diameter = inputs.bullet_diameter;
289 inputs.bullet_mass = inputs.bullet_mass;
290 inputs.muzzle_angle = inputs.muzzle_angle;
291 inputs.caliber_inches = inputs.bullet_diameter / 0.0254;
292 inputs.weight_grains = inputs.bullet_mass / 0.00006479891;
293
294 let cluster_bc = if inputs.use_cluster_bc {
296 Some(ClusterBCDegradation::new())
297 } else {
298 None
299 };
300
301 Self {
302 inputs,
303 wind,
304 atmosphere,
305 max_range: 1000.0,
306 time_step: 0.001,
307 cluster_bc,
308 }
309 }
310
311 pub fn set_max_range(&mut self, range: f64) {
312 self.max_range = range;
313 }
314
315 pub fn set_time_step(&mut self, step: f64) {
316 self.time_step = step;
317 }
318
319 fn get_wind_at_altitude(&self, altitude_m: f64) -> Vector3<f64> {
320 let profile = WindShearProfile {
322 model: if self.inputs.wind_shear_model == "logarithmic" {
323 WindShearModel::Logarithmic
324 } else if self.inputs.wind_shear_model == "power" {
325 WindShearModel::PowerLaw
326 } else {
327 WindShearModel::PowerLaw },
329 surface_wind: WindLayer {
330 altitude_m: 0.0,
331 speed_mps: self.wind.speed,
332 direction_deg: self.wind.direction.to_degrees(),
333 },
334 reference_height: 10.0, roughness_length: 0.03, power_exponent: 1.0 / 7.0, geostrophic_wind: None,
338 custom_layers: Vec::new(),
339 };
340
341 profile.get_wind_at_altitude(altitude_m)
342 }
343
344 pub fn solve(&self) -> Result<TrajectoryResult, BallisticsError> {
345 if self.inputs.use_rk4 {
346 if self.inputs.use_adaptive_rk45 {
347 self.solve_rk45()
348 } else {
349 self.solve_rk4()
350 }
351 } else {
352 self.solve_euler()
353 }
354 }
355
356 fn solve_euler(&self) -> Result<TrajectoryResult, BallisticsError> {
357 let mut time = 0.0;
359 let mut position = Vector3::new(0.0, self.inputs.sight_height + self.inputs.muzzle_height, 0.0);
360 let horizontal_velocity = self.inputs.muzzle_velocity * self.inputs.muzzle_angle.cos();
362 let mut velocity = Vector3::new(
363 horizontal_velocity * self.inputs.azimuth_angle.sin(), self.inputs.muzzle_velocity * self.inputs.muzzle_angle.sin(), horizontal_velocity * self.inputs.azimuth_angle.cos(), );
367
368 let mut points = Vec::new();
369 let mut max_height = position.y;
370 let mut min_pitch_damping = 1.0; let mut transonic_mach = None; let mut angular_state = if self.inputs.enable_precession_nutation {
375 Some(AngularState {
376 pitch_angle: 0.001, yaw_angle: 0.001,
378 pitch_rate: 0.0,
379 yaw_rate: 0.0,
380 precession_angle: 0.0,
381 nutation_phase: 0.0,
382 })
383 } else {
384 None
385 };
386 let mut max_yaw_angle = 0.0;
387 let mut max_precession_angle = 0.0;
388
389 let air_density = calculate_air_density(&self.atmosphere);
391
392 let wind_vector = Vector3::new(
394 self.wind.speed * self.wind.direction.sin(),
395 0.0,
396 self.wind.speed * self.wind.direction.cos(),
397 );
398
399 while position.z < self.max_range && position.y >= 0.0 && time < 100.0 {
401 let velocity_magnitude = velocity.magnitude();
403 let kinetic_energy = 0.5 * self.inputs.bullet_mass * velocity_magnitude * velocity_magnitude;
404
405 points.push(TrajectoryPoint {
406 time,
407 position: position.clone(),
408 velocity_magnitude,
409 kinetic_energy,
410 });
411
412 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.z,
542 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.z,
556 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.sin(),
579 self.inputs.muzzle_velocity * self.inputs.muzzle_angle.sin(),
580 horizontal_velocity * self.inputs.azimuth_angle.cos(),
581 );
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.sin(),
610 0.0,
611 self.wind.speed * self.wind.direction.cos(),
612 );
613
614 while position.z < 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.z,
754 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.z,
768 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.sin(),
791 self.inputs.muzzle_velocity * self.inputs.muzzle_angle.sin(),
792 horizontal_velocity * self.inputs.azimuth_angle.cos(),
793 );
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.z < self.max_range && position.y > self.inputs.ground_threshold && time < 100.0 {
808 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.sin(),
832 0.0,
833 -self.wind.speed * self.wind.direction.cos(),
834 );
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.z,
864 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 let reference_area = std::f64::consts::PI * (self.inputs.bullet_diameter / 2.0).powi(2);
999
1000 let effective_bc = if let Some(ref cluster_bc) = self.cluster_bc {
1002 let velocity_fps = velocity_magnitude * 3.28084;
1004 cluster_bc.apply_correction(
1005 self.inputs.bc_value,
1006 self.inputs.caliber_inches * 0.0254, self.inputs.weight_grains,
1008 velocity_fps
1009 )
1010 } else {
1011 self.inputs.bc_value
1012 };
1013
1014 let drag_magnitude = 0.5 * air_density * velocity_magnitude.powi(2) * cd * reference_area / effective_bc;
1015
1016 let drag_force = -relative_velocity.normalize() * drag_magnitude;
1018
1019 let acceleration = drag_force / self.inputs.bullet_mass + Vector3::new(0.0, -9.81, 0.0);
1021
1022 acceleration
1023 }
1024
1025 fn calculate_drag_coefficient(&self, velocity: f64) -> f64 {
1026 let temp_c = self.atmosphere.temperature;
1029 let temp_k = temp_c + 273.15;
1030 let gamma = 1.4; let r_specific = 287.05; let speed_of_sound = (gamma * r_specific * temp_k).sqrt();
1033 let mach = velocity / speed_of_sound;
1034
1035 let base_cd = match self.inputs.bc_type {
1037 DragModel::G1 => 0.5,
1038 DragModel::G7 => 0.4,
1039 _ => 0.45,
1040 };
1041
1042 let projectile_shape = if let Some(ref model) = self.inputs.bullet_model {
1044 if model.to_lowercase().contains("boat") || model.to_lowercase().contains("bt") {
1046 ProjectileShape::BoatTail
1047 } else if model.to_lowercase().contains("round") || model.to_lowercase().contains("rn") {
1048 ProjectileShape::RoundNose
1049 } else if model.to_lowercase().contains("flat") || model.to_lowercase().contains("fb") {
1050 ProjectileShape::FlatBase
1051 } else {
1052 get_projectile_shape(
1054 self.inputs.bullet_diameter,
1055 self.inputs.bullet_mass / 0.00006479891, &self.inputs.bc_type.to_string()
1057 )
1058 }
1059 } else {
1060 get_projectile_shape(
1062 self.inputs.bullet_diameter,
1063 self.inputs.bullet_mass / 0.00006479891, &self.inputs.bc_type.to_string()
1065 )
1066 };
1067
1068 let include_wave_drag = self.inputs.enable_advanced_effects;
1071 transonic_correction(mach, base_cd, projectile_shape, include_wave_drag)
1072 }
1073}
1074
1075#[derive(Debug, Clone)]
1077pub struct MonteCarloParams {
1078 pub num_simulations: usize,
1079 pub velocity_std_dev: f64,
1080 pub angle_std_dev: f64,
1081 pub bc_std_dev: f64,
1082 pub wind_speed_std_dev: f64,
1083 pub target_distance: Option<f64>,
1084 pub base_wind_speed: f64,
1085 pub base_wind_direction: f64,
1086 pub azimuth_std_dev: f64, }
1088
1089impl Default for MonteCarloParams {
1090 fn default() -> Self {
1091 Self {
1092 num_simulations: 1000,
1093 velocity_std_dev: 1.0,
1094 angle_std_dev: 0.001,
1095 bc_std_dev: 0.01,
1096 wind_speed_std_dev: 1.0,
1097 target_distance: None,
1098 base_wind_speed: 0.0,
1099 base_wind_direction: 0.0,
1100 azimuth_std_dev: 0.001, }
1102 }
1103}
1104
1105#[derive(Debug, Clone)]
1107pub struct MonteCarloResults {
1108 pub ranges: Vec<f64>,
1109 pub impact_velocities: Vec<f64>,
1110 pub impact_positions: Vec<Vector3<f64>>,
1111}
1112
1113pub fn run_monte_carlo(
1115 base_inputs: BallisticInputs,
1116 params: MonteCarloParams,
1117) -> Result<MonteCarloResults, BallisticsError> {
1118 let base_wind = WindConditions {
1119 speed: params.base_wind_speed,
1120 direction: params.base_wind_direction,
1121 };
1122 run_monte_carlo_with_wind(base_inputs, base_wind, params)
1123}
1124
1125pub fn run_monte_carlo_with_wind(
1127 base_inputs: BallisticInputs,
1128 base_wind: WindConditions,
1129 params: MonteCarloParams,
1130) -> Result<MonteCarloResults, BallisticsError> {
1131 use rand::thread_rng;
1132 use rand_distr::{Distribution, Normal};
1133
1134 let mut rng = thread_rng();
1135 let mut ranges = Vec::new();
1136 let mut impact_velocities = Vec::new();
1137 let mut impact_positions = Vec::new();
1138
1139 let baseline_solver = TrajectorySolver::new(base_inputs.clone(), base_wind.clone(), Default::default());
1141 let baseline_result = baseline_solver.solve()?;
1142 let baseline_impact = baseline_result.points.last()
1143 .ok_or("No baseline trajectory points")?
1144 .position.clone();
1145
1146 let velocity_dist = Normal::new(base_inputs.muzzle_velocity, params.velocity_std_dev)
1148 .map_err(|e| format!("Invalid velocity distribution: {}", e))?;
1149 let angle_dist = Normal::new(base_inputs.muzzle_angle, params.angle_std_dev)
1150 .map_err(|e| format!("Invalid angle distribution: {}", e))?;
1151 let bc_dist = Normal::new(base_inputs.bc_value, params.bc_std_dev)
1152 .map_err(|e| format!("Invalid BC distribution: {}", e))?;
1153 let wind_speed_dist = Normal::new(base_wind.speed, params.wind_speed_std_dev)
1154 .map_err(|e| format!("Invalid wind speed distribution: {}", e))?;
1155 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))?;
1157 let azimuth_dist = Normal::new(base_inputs.azimuth_angle, params.azimuth_std_dev)
1158 .map_err(|e| format!("Invalid azimuth distribution: {}", e))?;
1159
1160 let distance = baseline_impact.z; let pointing_error_dist = Normal::new(0.0, params.angle_std_dev * distance)
1163 .map_err(|e| format!("Invalid pointing distribution: {}", e))?;
1164
1165 for _ in 0..params.num_simulations {
1166 let mut inputs = base_inputs.clone();
1168 inputs.muzzle_velocity = velocity_dist.sample(&mut rng).max(0.0);
1169 inputs.muzzle_angle = angle_dist.sample(&mut rng);
1170 inputs.bc_value = bc_dist.sample(&mut rng).max(0.01);
1171 inputs.azimuth_angle = azimuth_dist.sample(&mut rng); let wind = WindConditions {
1175 speed: wind_speed_dist.sample(&mut rng).abs(),
1176 direction: wind_dir_dist.sample(&mut rng),
1177 };
1178
1179 let solver = TrajectorySolver::new(inputs, wind, Default::default());
1181 match solver.solve() {
1182 Ok(result) => {
1183 ranges.push(result.max_range);
1184 impact_velocities.push(result.impact_velocity);
1185 if let Some(last_point) = result.points.last() {
1186 let mut deviation = Vector3::new(
1188 last_point.position.x - baseline_impact.x, last_point.position.y - baseline_impact.y, last_point.position.z - baseline_impact.z, );
1192
1193 let pointing_error_y = pointing_error_dist.sample(&mut rng);
1196 deviation.y += pointing_error_y;
1197
1198 impact_positions.push(deviation);
1199 }
1200 },
1201 Err(_) => {
1202 continue;
1204 }
1205 }
1206 }
1207
1208 if ranges.is_empty() {
1209 return Err("No successful simulations".into());
1210 }
1211
1212 Ok(MonteCarloResults {
1213 ranges,
1214 impact_velocities,
1215 impact_positions,
1216 })
1217}
1218
1219pub fn calculate_zero_angle(
1221 inputs: BallisticInputs,
1222 target_distance: f64,
1223 target_height: f64,
1224) -> Result<f64, BallisticsError> {
1225 calculate_zero_angle_with_conditions(
1226 inputs,
1227 target_distance,
1228 target_height,
1229 WindConditions::default(),
1230 AtmosphericConditions::default(),
1231 )
1232}
1233
1234pub fn calculate_zero_angle_with_conditions(
1235 inputs: BallisticInputs,
1236 target_distance: f64,
1237 target_height: f64,
1238 wind: WindConditions,
1239 atmosphere: AtmosphericConditions,
1240) -> Result<f64, BallisticsError> {
1241 let mut low_angle = -0.2; let mut high_angle = 0.2; let tolerance = 0.00001; let max_iterations = 50;
1246
1247 for iteration in 0..max_iterations {
1248 let mid_angle = (low_angle + high_angle) / 2.0;
1249
1250 let mut test_inputs = inputs.clone();
1251 test_inputs.muzzle_angle = mid_angle;
1252
1253 let mut solver = TrajectorySolver::new(test_inputs, wind.clone(), atmosphere.clone());
1254 solver.set_max_range(target_distance * 2.0);
1256 solver.set_time_step(0.001);
1257 let result = solver.solve()?;
1258
1259 eprintln!(" Iteration {}: angle = {:.6} rad ({:.4} deg)",
1260 iteration, mid_angle, mid_angle * 180.0 / std::f64::consts::PI);
1261
1262 let mut height_at_target = None;
1264 for i in 0..result.points.len() {
1265 if result.points[i].position.z >= target_distance {
1266 if i > 0 {
1267 let p1 = &result.points[i - 1];
1269 let p2 = &result.points[i];
1270 let t = (target_distance - p1.position.z) / (p2.position.z - p1.position.z);
1271 height_at_target = Some(p1.position.y + t * (p2.position.y - p1.position.y));
1272 } else {
1273 height_at_target = Some(result.points[i].position.y);
1274 }
1275 break;
1276 }
1277 }
1278
1279 match height_at_target {
1280 Some(height) => {
1281 let error = height - target_height;
1282 eprintln!(" Height at target: {:.6} m, target: {:.6} m, error: {:.6} m",
1283 height, target_height, error);
1284 if error.abs() < 0.001 {
1285 eprintln!(" Converged!");
1286 return Ok(mid_angle);
1287 }
1288
1289 if error > 0.0 {
1290 high_angle = mid_angle;
1291 } else {
1292 low_angle = mid_angle;
1293 }
1294 },
1295 None => {
1296 low_angle = mid_angle;
1298 }
1299 }
1300
1301 if (high_angle - low_angle).abs() < tolerance {
1302 return Ok(mid_angle);
1303 }
1304 }
1305
1306 Err("Failed to find zero angle".into())
1307}
1308
1309pub fn estimate_bc_from_trajectory(
1311 velocity: f64,
1312 mass: f64,
1313 diameter: f64,
1314 points: &[(f64, f64)], ) -> Result<f64, BallisticsError> {
1316 let mut best_bc = 0.5;
1318 let mut best_error = f64::MAX;
1319 let mut found_valid = false;
1320
1321 for bc in (100..1000).step_by(10) {
1323 let bc_value = bc as f64 / 1000.0;
1324
1325 let inputs = BallisticInputs {
1326 muzzle_velocity: velocity,
1327 bc_value,
1328 bullet_mass: mass,
1329 bullet_diameter: diameter,
1330 ..Default::default()
1331 };
1332
1333 let mut solver = TrajectorySolver::new(inputs, Default::default(), Default::default());
1334 solver.set_max_range(points.last().map(|(d, _)| *d * 1.5).unwrap_or(1000.0));
1336
1337 let result = match solver.solve() {
1338 Ok(r) => r,
1339 Err(_) => continue, };
1341
1342 let mut total_error = 0.0;
1344 for (target_dist, target_drop) in points {
1345 let mut calculated_drop = None;
1347 for i in 0..result.points.len() {
1348 if result.points[i].position.z >= *target_dist {
1349 if i > 0 {
1350 let p1 = &result.points[i - 1];
1352 let p2 = &result.points[i];
1353 let t = (target_dist - p1.position.z) / (p2.position.z - p1.position.z);
1354 calculated_drop = Some(-(p1.position.y + t * (p2.position.y - p1.position.y)));
1355 } else {
1356 calculated_drop = Some(-result.points[i].position.y);
1357 }
1358 break;
1359 }
1360 }
1361
1362 if let Some(drop) = calculated_drop {
1363 let error = (drop - target_drop).abs();
1364 total_error += error * error;
1365 }
1366 }
1367
1368 if total_error < best_error {
1369 best_error = total_error;
1370 best_bc = bc_value;
1371 found_valid = true;
1372 }
1373 }
1374
1375 if !found_valid {
1376 return Err(BallisticsError::from("Unable to estimate BC from provided data. Check that drop values are in correct units.".to_string()));
1377 }
1378
1379 Ok(best_bc)
1380}
1381
1382fn calculate_air_density(atmosphere: &AtmosphericConditions) -> f64 {
1384 let r_specific = 287.058; let temperature_k = atmosphere.temperature + 273.15;
1388
1389 let pressure_pa = atmosphere.pressure * 100.0;
1391
1392 let density = pressure_pa / (r_specific * temperature_k);
1394
1395 let altitude_factor = (-atmosphere.altitude / 8000.0).exp();
1397
1398 density * altitude_factor
1399}
1400
1401use rand;
1403use rand_distr;