1use crate::cluster_bc::ClusterBCDegradation;
3use crate::pitch_damping::{calculate_pitch_damping_coefficient, PitchDampingCoefficients};
4use crate::precession_nutation::{
5 calculate_combined_angular_motion, AngularState, PrecessionNutationParams,
6};
7use crate::trajectory_sampling::{
8 sample_trajectory, TrajectoryData, TrajectoryOutputs, TrajectorySample,
9};
10use crate::transonic_drag::{get_projectile_shape, transonic_correction, ProjectileShape};
11use crate::wind_shear::{WindLayer, WindShearModel, WindShearProfile};
12use crate::DragModel;
13use nalgebra::Vector3;
14use std::error::Error;
15use std::fmt;
16
17#[derive(Debug, Clone, Copy, PartialEq)]
19pub enum UnitSystem {
20 Imperial,
21 Metric,
22}
23
24#[derive(Debug, Clone, Copy, PartialEq)]
26pub enum OutputFormat {
27 Table,
28 Json,
29 Csv,
30}
31
32#[derive(Debug)]
34pub struct BallisticsError {
35 message: String,
36}
37
38impl fmt::Display for BallisticsError {
39 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
40 write!(f, "{}", self.message)
41 }
42}
43
44impl Error for BallisticsError {}
45
46impl From<String> for BallisticsError {
47 fn from(msg: String) -> Self {
48 BallisticsError { message: msg }
49 }
50}
51
52impl From<&str> for BallisticsError {
53 fn from(msg: &str) -> Self {
54 BallisticsError {
55 message: msg.to_string(),
56 }
57 }
58}
59
60#[derive(Debug, Clone)]
64pub struct BallisticInputs {
65 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,
110 pub use_powder_sensitivity: bool,
111 pub powder_temp_sensitivity: f64,
112 pub powder_temp: f64, pub tipoff_yaw: f64, pub tipoff_decay_distance: f64, pub use_bc_segments: bool,
116 pub bc_segments: Option<Vec<(f64, f64)>>, pub bc_segments_data: Option<Vec<crate::BCSegmentData>>, pub use_enhanced_spin_drift: bool,
119 pub use_form_factor: bool,
120 pub enable_wind_shear: bool,
121 pub wind_shear_model: String,
122 pub enable_trajectory_sampling: bool,
123 pub sample_interval: f64, pub enable_pitch_damping: bool,
125 pub enable_precession_nutation: bool,
126 pub use_cluster_bc: bool, pub custom_drag_table: Option<crate::drag::DragTable>,
130
131 pub bc_type_str: Option<String>,
133}
134
135impl Default for BallisticInputs {
136 fn default() -> Self {
137 let mass_kg = 0.01;
138 let diameter_m = 0.00762;
139 let bc = 0.5;
140 let muzzle_angle_rad = 0.0;
141 let bc_type = DragModel::G1;
142
143 Self {
144 bc_value: bc,
146 bc_type,
147 bullet_mass: mass_kg,
148 muzzle_velocity: 800.0,
149 bullet_diameter: diameter_m,
150 bullet_length: diameter_m * 4.0, muzzle_angle: muzzle_angle_rad,
154 target_distance: 100.0,
155 azimuth_angle: 0.0,
156 shooting_angle: 0.0,
157 sight_height: 0.05,
158 muzzle_height: 0.0, target_height: 0.0, ground_threshold: -100.0, altitude: 0.0,
164 temperature: 15.0,
165 pressure: 1013.25, humidity: 0.5, latitude: None,
168
169 wind_speed: 0.0,
171 wind_angle: 0.0,
172
173 twist_rate: 12.0, is_twist_right: true,
176 caliber_inches: diameter_m / 0.0254, weight_grains: mass_kg / 0.00006479891, manufacturer: None,
179 bullet_model: None,
180 bullet_id: None,
181 bullet_cluster: None,
182
183 use_rk4: true, use_adaptive_rk45: true, enable_advanced_effects: false,
189 use_powder_sensitivity: false,
190 powder_temp_sensitivity: 0.0,
191 powder_temp: 15.0,
192 tipoff_yaw: 0.0,
193 tipoff_decay_distance: 50.0,
194 use_bc_segments: false,
195 bc_segments: None,
196 bc_segments_data: None,
197 use_enhanced_spin_drift: false,
198 use_form_factor: false,
199 enable_wind_shear: false,
200 wind_shear_model: "none".to_string(),
201 enable_trajectory_sampling: false,
202 sample_interval: 10.0, enable_pitch_damping: false,
204 enable_precession_nutation: false,
205 use_cluster_bc: false, custom_drag_table: None,
209
210 bc_type_str: None,
212 }
213 }
214}
215
216#[derive(Debug, Clone)]
218pub struct WindConditions {
219 pub speed: f64, pub direction: f64, }
222
223impl Default for WindConditions {
224 fn default() -> Self {
225 Self {
226 speed: 0.0,
227 direction: 0.0,
228 }
229 }
230}
231
232#[derive(Debug, Clone)]
234pub struct AtmosphericConditions {
235 pub temperature: f64, pub pressure: f64, pub humidity: f64, pub altitude: f64, }
240
241impl Default for AtmosphericConditions {
242 fn default() -> Self {
243 Self {
244 temperature: 15.0,
245 pressure: 1013.25,
246 humidity: 50.0,
247 altitude: 0.0,
248 }
249 }
250}
251
252#[derive(Debug, Clone)]
254pub struct TrajectoryPoint {
255 pub time: f64,
256 pub position: Vector3<f64>,
257 pub velocity_magnitude: f64,
258 pub kinetic_energy: f64,
259}
260
261#[derive(Debug, Clone)]
263pub struct TrajectoryResult {
264 pub max_range: f64,
265 pub max_height: f64,
266 pub time_of_flight: f64,
267 pub impact_velocity: f64,
268 pub impact_energy: f64,
269 pub points: Vec<TrajectoryPoint>,
270 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>, }
277
278impl TrajectoryResult {
279 pub fn position_at_range(&self, target_range: f64) -> Option<Vector3<f64>> {
283 if self.points.is_empty() {
284 return None;
285 }
286
287 for i in 0..self.points.len() - 1 {
289 let p1 = &self.points[i];
290 let p2 = &self.points[i + 1];
291
292 if p1.position.z <= target_range && p2.position.z >= target_range {
294 let dz = p2.position.z - p1.position.z;
296 if dz.abs() < 1e-10 {
297 return Some(p1.position);
298 }
299 let t = (target_range - p1.position.z) / dz;
300
301 return Some(Vector3::new(
303 p1.position.x + t * (p2.position.x - p1.position.x),
304 p1.position.y + t * (p2.position.y - p1.position.y),
305 target_range,
306 ));
307 }
308 }
309
310 self.points.last().map(|p| p.position)
312 }
313}
314
315pub struct TrajectorySolver {
317 inputs: BallisticInputs,
318 wind: WindConditions,
319 atmosphere: AtmosphericConditions,
320 max_range: f64,
321 time_step: f64,
322 cluster_bc: Option<ClusterBCDegradation>,
323}
324
325impl TrajectorySolver {
326 pub fn new(
327 mut inputs: BallisticInputs,
328 wind: WindConditions,
329 atmosphere: AtmosphericConditions,
330 ) -> Self {
331 inputs.caliber_inches = inputs.bullet_diameter / 0.0254;
333 inputs.weight_grains = inputs.bullet_mass / 0.00006479891;
334
335 let cluster_bc = if inputs.use_cluster_bc {
337 Some(ClusterBCDegradation::new())
338 } else {
339 None
340 };
341
342 Self {
343 inputs,
344 wind,
345 atmosphere,
346 max_range: 1000.0,
347 time_step: 0.001,
348 cluster_bc,
349 }
350 }
351
352 pub fn set_max_range(&mut self, range: f64) {
353 self.max_range = range;
354 }
355
356 pub fn set_time_step(&mut self, step: f64) {
357 self.time_step = step;
358 }
359
360 fn get_wind_at_altitude(&self, altitude_m: f64) -> Vector3<f64> {
361 let profile = WindShearProfile {
363 model: if self.inputs.wind_shear_model == "logarithmic" {
364 WindShearModel::Logarithmic
365 } else if self.inputs.wind_shear_model == "power" {
366 WindShearModel::PowerLaw
367 } else {
368 WindShearModel::PowerLaw },
370 surface_wind: WindLayer {
371 altitude_m: 0.0,
372 speed_mps: self.wind.speed,
373 direction_deg: self.wind.direction.to_degrees(),
374 },
375 reference_height: 10.0, roughness_length: 0.03, power_exponent: 1.0 / 7.0, geostrophic_wind: None,
379 custom_layers: Vec::new(),
380 };
381
382 profile.get_wind_at_altitude(altitude_m)
383 }
384
385 pub fn solve(&self) -> Result<TrajectoryResult, BallisticsError> {
386 if self.inputs.use_rk4 {
387 if self.inputs.use_adaptive_rk45 {
388 self.solve_rk45()
389 } else {
390 self.solve_rk4()
391 }
392 } else {
393 self.solve_euler()
394 }
395 }
396
397 fn solve_euler(&self) -> Result<TrajectoryResult, BallisticsError> {
398 let mut time = 0.0;
400 let mut position = Vector3::new(
403 0.0,
404 self.inputs.muzzle_height, 0.0,
406 );
407 let horizontal_velocity = self.inputs.muzzle_velocity * self.inputs.muzzle_angle.cos();
410 let mut velocity = Vector3::new(
411 horizontal_velocity * self.inputs.azimuth_angle.sin(), self.inputs.muzzle_velocity * self.inputs.muzzle_angle.sin(), horizontal_velocity * self.inputs.azimuth_angle.cos(), );
415
416 let mut points = Vec::new();
417 let mut max_height = position.y;
418 let mut min_pitch_damping = 1.0; let mut transonic_mach = None; let mut angular_state = if self.inputs.enable_precession_nutation {
423 Some(AngularState {
424 pitch_angle: 0.001, yaw_angle: 0.001,
426 pitch_rate: 0.0,
427 yaw_rate: 0.0,
428 precession_angle: 0.0,
429 nutation_phase: 0.0,
430 })
431 } else {
432 None
433 };
434 let mut max_yaw_angle = 0.0;
435 let mut max_precession_angle = 0.0;
436
437 let air_density = calculate_air_density(&self.atmosphere);
439
440 let wind_vector = Vector3::new(
442 self.wind.speed * self.wind.direction.sin(), 0.0,
444 self.wind.speed * self.wind.direction.cos(), );
446
447 while position.z < self.max_range && position.y >= 0.0 && time < 100.0 {
449 let velocity_magnitude = velocity.magnitude();
451 let kinetic_energy =
452 0.5 * self.inputs.bullet_mass * velocity_magnitude * velocity_magnitude;
453
454 points.push(TrajectoryPoint {
455 time,
456 position: position,
457 velocity_magnitude,
458 kinetic_energy,
459 });
460
461 if points.len() == 1 || points.len() % 100 == 0 {
464 eprintln!("Trajectory point {}: time={:.3}s, lateral={:.2}m, vertical={:.2}m, downrange={:.2}m, vel={:.1}m/s",
465 points.len(), time, position.x, position.y, position.z, velocity_magnitude);
466 }
467
468 if position.y > max_height {
470 max_height = position.y;
471 }
472
473 if self.inputs.enable_pitch_damping {
475 let temp_c = self.atmosphere.temperature;
476 let temp_k = temp_c + 273.15;
477 let speed_of_sound = (1.4 * 287.05 * temp_k).sqrt();
478 let mach = velocity_magnitude / speed_of_sound;
479
480 if transonic_mach.is_none() && mach < 1.2 && mach > 0.8 {
482 transonic_mach = Some(mach);
483 }
484
485 let bullet_type = if let Some(ref model) = self.inputs.bullet_model {
487 model.as_str()
488 } else {
489 "default"
490 };
491 let coeffs = PitchDampingCoefficients::from_bullet_type(bullet_type);
492 let pitch_damping = calculate_pitch_damping_coefficient(mach, &coeffs);
493
494 if pitch_damping < min_pitch_damping {
496 min_pitch_damping = pitch_damping;
497 }
498 }
499
500 if self.inputs.enable_precession_nutation {
502 if let Some(ref mut state) = angular_state {
503 let velocity_magnitude = velocity.magnitude();
504 let temp_c = self.atmosphere.temperature;
505 let temp_k = temp_c + 273.15;
506 let speed_of_sound = (1.4 * 287.05 * temp_k).sqrt();
507 let mach = velocity_magnitude / speed_of_sound;
508
509 let spin_rate_rad_s = if self.inputs.twist_rate > 0.0 {
511 let velocity_fps = velocity_magnitude * 3.28084;
512 let twist_rate_ft = self.inputs.twist_rate / 12.0;
513 (velocity_fps / twist_rate_ft) * 2.0 * std::f64::consts::PI
514 } else {
515 0.0
516 };
517
518 let params = PrecessionNutationParams {
520 mass_kg: self.inputs.bullet_mass,
521 caliber_m: self.inputs.bullet_diameter,
522 length_m: self.inputs.bullet_length,
523 spin_rate_rad_s,
524 spin_inertia: 6.94e-8, transverse_inertia: 9.13e-7, velocity_mps: velocity_magnitude,
527 air_density_kg_m3: air_density,
528 mach,
529 pitch_damping_coeff: -0.8,
530 nutation_damping_factor: 0.05,
531 };
532
533 *state = calculate_combined_angular_motion(
535 ¶ms,
536 state,
537 time,
538 self.time_step,
539 0.001, );
541
542 if state.yaw_angle.abs() > max_yaw_angle {
544 max_yaw_angle = state.yaw_angle.abs();
545 }
546 if state.precession_angle.abs() > max_precession_angle {
547 max_precession_angle = state.precession_angle.abs();
548 }
549 }
550 }
551
552 let actual_wind = if self.inputs.enable_wind_shear {
554 self.get_wind_at_altitude(position.y)
555 } else {
556 wind_vector
557 };
558 let velocity_rel = velocity - actual_wind;
559 let velocity_rel_mag = velocity_rel.magnitude();
560 let drag_coefficient = self.calculate_drag_coefficient(velocity_rel_mag);
561
562 let drag_force = 0.5
564 * air_density
565 * drag_coefficient
566 * self.inputs.bullet_diameter
567 * self.inputs.bullet_diameter
568 * std::f64::consts::PI
569 / 4.0
570 * velocity_rel_mag
571 * velocity_rel_mag;
572
573 let drag_acceleration = -drag_force / self.inputs.bullet_mass;
575 let acceleration = Vector3::new(
576 drag_acceleration * velocity_rel.x / velocity_rel_mag,
577 drag_acceleration * velocity_rel.y / velocity_rel_mag - 9.80665,
578 drag_acceleration * velocity_rel.z / velocity_rel_mag,
579 );
580
581 velocity += acceleration * self.time_step;
583 position += velocity * self.time_step;
584 time += self.time_step;
585 }
586
587 let last_point = points.last().ok_or("No trajectory points generated")?;
589
590 let sampled_points = if self.inputs.enable_trajectory_sampling {
592 let trajectory_data = TrajectoryData {
593 times: points.iter().map(|p| p.time).collect(),
594 positions: points.iter().map(|p| p.position).collect(),
595 velocities: points
596 .iter()
597 .map(|p| {
598 Vector3::new(0.0, 0.0, p.velocity_magnitude)
600 })
601 .collect(),
602 transonic_distances: Vec::new(), };
604
605 let sight_position_m = self.inputs.muzzle_height + self.inputs.sight_height;
610 let outputs = TrajectoryOutputs {
611 target_distance_horiz_m: last_point.position.z, target_vertical_height_m: sight_position_m,
613 time_of_flight_s: last_point.time,
614 max_ord_dist_horiz_m: max_height,
615 sight_height_m: sight_position_m,
616 };
617
618 let samples = sample_trajectory(
620 &trajectory_data,
621 &outputs,
622 self.inputs.sample_interval,
623 self.inputs.bullet_mass,
624 );
625 Some(samples)
626 } else {
627 None
628 };
629
630 Ok(TrajectoryResult {
631 max_range: last_point.position.z, max_height,
633 time_of_flight: last_point.time,
634 impact_velocity: last_point.velocity_magnitude,
635 impact_energy: last_point.kinetic_energy,
636 points,
637 sampled_points,
638 min_pitch_damping: if self.inputs.enable_pitch_damping {
639 Some(min_pitch_damping)
640 } else {
641 None
642 },
643 transonic_mach,
644 angular_state,
645 max_yaw_angle: if self.inputs.enable_precession_nutation {
646 Some(max_yaw_angle)
647 } else {
648 None
649 },
650 max_precession_angle: if self.inputs.enable_precession_nutation {
651 Some(max_precession_angle)
652 } else {
653 None
654 },
655 })
656 }
657
658 fn solve_rk4(&self) -> Result<TrajectoryResult, BallisticsError> {
659 let mut time = 0.0;
661 let mut position = Vector3::new(
665 0.0,
666 self.inputs.muzzle_height, 0.0,
668 );
669
670 let horizontal_velocity = self.inputs.muzzle_velocity * self.inputs.muzzle_angle.cos();
673 let mut velocity = Vector3::new(
674 horizontal_velocity * self.inputs.azimuth_angle.sin(), self.inputs.muzzle_velocity * self.inputs.muzzle_angle.sin(), horizontal_velocity * self.inputs.azimuth_angle.cos(), );
678
679 let mut points = Vec::new();
680 let mut max_height = position.y;
681 let mut min_pitch_damping = 1.0; let mut transonic_mach = None; let mut angular_state = if self.inputs.enable_precession_nutation {
686 Some(AngularState {
687 pitch_angle: 0.001, yaw_angle: 0.001,
689 pitch_rate: 0.0,
690 yaw_rate: 0.0,
691 precession_angle: 0.0,
692 nutation_phase: 0.0,
693 })
694 } else {
695 None
696 };
697 let mut max_yaw_angle = 0.0;
698 let mut max_precession_angle = 0.0;
699
700 let air_density = calculate_air_density(&self.atmosphere);
702
703 let wind_vector = Vector3::new(
705 self.wind.speed * self.wind.direction.sin(), 0.0,
707 self.wind.speed * self.wind.direction.cos(), );
709
710 while position.z < self.max_range && position.y >= 0.0 && time < 100.0 {
712 let velocity_magnitude = velocity.magnitude();
714 let kinetic_energy =
715 0.5 * self.inputs.bullet_mass * velocity_magnitude * velocity_magnitude;
716
717 points.push(TrajectoryPoint {
718 time,
719 position: position,
720 velocity_magnitude,
721 kinetic_energy,
722 });
723
724 if position.y > max_height {
725 max_height = position.y;
726 }
727
728 if self.inputs.enable_pitch_damping {
730 let temp_c = self.atmosphere.temperature;
731 let temp_k = temp_c + 273.15;
732 let speed_of_sound = (1.4 * 287.05 * temp_k).sqrt();
733 let mach = velocity_magnitude / speed_of_sound;
734
735 if transonic_mach.is_none() && mach < 1.2 && mach > 0.8 {
737 transonic_mach = Some(mach);
738 }
739
740 let bullet_type = if let Some(ref model) = self.inputs.bullet_model {
742 model.as_str()
743 } else {
744 "default"
745 };
746 let coeffs = PitchDampingCoefficients::from_bullet_type(bullet_type);
747 let pitch_damping = calculate_pitch_damping_coefficient(mach, &coeffs);
748
749 if pitch_damping < min_pitch_damping {
751 min_pitch_damping = pitch_damping;
752 }
753 }
754
755 if self.inputs.enable_precession_nutation {
757 if let Some(ref mut state) = angular_state {
758 let velocity_magnitude = velocity.magnitude();
759 let temp_c = self.atmosphere.temperature;
760 let temp_k = temp_c + 273.15;
761 let speed_of_sound = (1.4 * 287.05 * temp_k).sqrt();
762 let mach = velocity_magnitude / speed_of_sound;
763
764 let spin_rate_rad_s = if self.inputs.twist_rate > 0.0 {
766 let velocity_fps = velocity_magnitude * 3.28084;
767 let twist_rate_ft = self.inputs.twist_rate / 12.0;
768 (velocity_fps / twist_rate_ft) * 2.0 * std::f64::consts::PI
769 } else {
770 0.0
771 };
772
773 let params = PrecessionNutationParams {
775 mass_kg: self.inputs.bullet_mass,
776 caliber_m: self.inputs.bullet_diameter,
777 length_m: self.inputs.bullet_length,
778 spin_rate_rad_s,
779 spin_inertia: 6.94e-8, transverse_inertia: 9.13e-7, velocity_mps: velocity_magnitude,
782 air_density_kg_m3: air_density,
783 mach,
784 pitch_damping_coeff: -0.8,
785 nutation_damping_factor: 0.05,
786 };
787
788 *state = calculate_combined_angular_motion(
790 ¶ms,
791 state,
792 time,
793 self.time_step,
794 0.001, );
796
797 if state.yaw_angle.abs() > max_yaw_angle {
799 max_yaw_angle = state.yaw_angle.abs();
800 }
801 if state.precession_angle.abs() > max_precession_angle {
802 max_precession_angle = state.precession_angle.abs();
803 }
804 }
805 }
806
807 let dt = self.time_step;
809
810 let acc1 = self.calculate_acceleration(&position, &velocity, air_density, &wind_vector);
812
813 let pos2 = position + velocity * (dt * 0.5);
815 let vel2 = velocity + acc1 * (dt * 0.5);
816 let acc2 = self.calculate_acceleration(&pos2, &vel2, air_density, &wind_vector);
817
818 let pos3 = position + vel2 * (dt * 0.5);
820 let vel3 = velocity + acc2 * (dt * 0.5);
821 let acc3 = self.calculate_acceleration(&pos3, &vel3, air_density, &wind_vector);
822
823 let pos4 = position + vel3 * dt;
825 let vel4 = velocity + acc3 * dt;
826 let acc4 = self.calculate_acceleration(&pos4, &vel4, air_density, &wind_vector);
827
828 position += (velocity + vel2 * 2.0 + vel3 * 2.0 + vel4) * (dt / 6.0);
830 velocity += (acc1 + acc2 * 2.0 + acc3 * 2.0 + acc4) * (dt / 6.0);
831 time += dt;
832 }
833
834 let last_point = points.last().ok_or("No trajectory points generated")?;
836
837 let sampled_points = if self.inputs.enable_trajectory_sampling {
839 let trajectory_data = TrajectoryData {
840 times: points.iter().map(|p| p.time).collect(),
841 positions: points.iter().map(|p| p.position).collect(),
842 velocities: points
843 .iter()
844 .map(|p| {
845 Vector3::new(0.0, 0.0, p.velocity_magnitude)
847 })
848 .collect(),
849 transonic_distances: Vec::new(), };
851
852 let sight_position_m = self.inputs.muzzle_height + self.inputs.sight_height;
857 let outputs = TrajectoryOutputs {
858 target_distance_horiz_m: last_point.position.z, target_vertical_height_m: sight_position_m,
860 time_of_flight_s: last_point.time,
861 max_ord_dist_horiz_m: max_height,
862 sight_height_m: sight_position_m,
863 };
864
865 let samples = sample_trajectory(
867 &trajectory_data,
868 &outputs,
869 self.inputs.sample_interval,
870 self.inputs.bullet_mass,
871 );
872 Some(samples)
873 } else {
874 None
875 };
876
877 Ok(TrajectoryResult {
878 max_range: last_point.position.z, max_height,
880 time_of_flight: last_point.time,
881 impact_velocity: last_point.velocity_magnitude,
882 impact_energy: last_point.kinetic_energy,
883 points,
884 sampled_points,
885 min_pitch_damping: if self.inputs.enable_pitch_damping {
886 Some(min_pitch_damping)
887 } else {
888 None
889 },
890 transonic_mach,
891 angular_state,
892 max_yaw_angle: if self.inputs.enable_precession_nutation {
893 Some(max_yaw_angle)
894 } else {
895 None
896 },
897 max_precession_angle: if self.inputs.enable_precession_nutation {
898 Some(max_precession_angle)
899 } else {
900 None
901 },
902 })
903 }
904
905 fn solve_rk45(&self) -> Result<TrajectoryResult, BallisticsError> {
906 let mut time = 0.0;
908 let mut position = Vector3::new(
911 0.0,
912 self.inputs.muzzle_height, 0.0,
914 );
915
916 let horizontal_velocity = self.inputs.muzzle_velocity * self.inputs.muzzle_angle.cos();
919 let mut velocity = Vector3::new(
920 horizontal_velocity * self.inputs.azimuth_angle.sin(), self.inputs.muzzle_velocity * self.inputs.muzzle_angle.sin(), horizontal_velocity * self.inputs.azimuth_angle.cos(), );
924
925 let mut points = Vec::new();
926 let mut max_height = position.y;
927 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;
935 const MAX_ITERATIONS: usize = 100000;
936
937 while position.z < self.max_range
938 && position.y > self.inputs.ground_threshold
939 && time < 100.0
940 {
941 iteration_count += 1;
943 if iteration_count > MAX_ITERATIONS {
944 break; }
946
947 let velocity_magnitude = velocity.magnitude();
949 let kinetic_energy = 0.5 * self.inputs.bullet_mass * velocity_magnitude.powi(2);
950
951 points.push(TrajectoryPoint {
952 time,
953 position: position,
954 velocity_magnitude,
955 kinetic_energy,
956 });
957
958 if position.y > max_height {
959 max_height = position.y;
960 }
961
962 let air_density = calculate_air_density(&self.atmosphere);
964 let wind_vector = Vector3::new(
965 self.wind.speed * self.wind.direction.sin(), 0.0,
967 self.wind.speed * self.wind.direction.cos(), );
969
970 let (new_pos, new_vel, new_dt) = self.rk45_step(
972 &position,
973 &velocity,
974 dt,
975 air_density,
976 &wind_vector,
977 tolerance,
978 );
979
980 dt = (safety_factor * new_dt).clamp(min_dt, max_dt);
982
983 position = new_pos;
985 velocity = new_vel;
986 time += dt;
987 }
988
989 if points.is_empty() {
991 return Err(BallisticsError::from("No trajectory points calculated"));
992 }
993
994 let last_point = points.last().unwrap();
995
996 let sampled_points = if self.inputs.enable_trajectory_sampling {
998 let trajectory_data = TrajectoryData {
1000 times: points.iter().map(|p| p.time).collect(),
1001 positions: points.iter().map(|p| p.position).collect(),
1002 velocities: points
1003 .iter()
1004 .map(|p| {
1005 Vector3::new(0.0, 0.0, p.velocity_magnitude)
1007 })
1008 .collect(),
1009 transonic_distances: Vec::new(),
1010 };
1011
1012 let sight_position_m = self.inputs.muzzle_height + self.inputs.sight_height;
1017 let outputs = TrajectoryOutputs {
1018 target_distance_horiz_m: last_point.position.z,
1019 target_vertical_height_m: sight_position_m,
1020 time_of_flight_s: last_point.time,
1021 max_ord_dist_horiz_m: max_height,
1022 sight_height_m: sight_position_m,
1023 };
1024
1025 let samples = sample_trajectory(
1026 &trajectory_data,
1027 &outputs,
1028 self.inputs.sample_interval,
1029 self.inputs.bullet_mass,
1030 );
1031 Some(samples)
1032 } else {
1033 None
1034 };
1035
1036 Ok(TrajectoryResult {
1037 max_range: last_point.position.z, max_height,
1039 time_of_flight: last_point.time,
1040 impact_velocity: last_point.velocity_magnitude,
1041 impact_energy: last_point.kinetic_energy,
1042 points,
1043 sampled_points,
1044 min_pitch_damping: None,
1045 transonic_mach: None,
1046 angular_state: None,
1047 max_yaw_angle: None,
1048 max_precession_angle: None,
1049 })
1050 }
1051
1052 fn rk45_step(
1053 &self,
1054 position: &Vector3<f64>,
1055 velocity: &Vector3<f64>,
1056 dt: f64,
1057 air_density: f64,
1058 wind_vector: &Vector3<f64>,
1059 tolerance: f64,
1060 ) -> (Vector3<f64>, Vector3<f64>, f64) {
1061 const A21: f64 = 1.0 / 5.0;
1063 const A31: f64 = 3.0 / 40.0;
1064 const A32: f64 = 9.0 / 40.0;
1065 const A41: f64 = 44.0 / 45.0;
1066 const A42: f64 = -56.0 / 15.0;
1067 const A43: f64 = 32.0 / 9.0;
1068 const A51: f64 = 19372.0 / 6561.0;
1069 const A52: f64 = -25360.0 / 2187.0;
1070 const A53: f64 = 64448.0 / 6561.0;
1071 const A54: f64 = -212.0 / 729.0;
1072 const A61: f64 = 9017.0 / 3168.0;
1073 const A62: f64 = -355.0 / 33.0;
1074 const A63: f64 = 46732.0 / 5247.0;
1075 const A64: f64 = 49.0 / 176.0;
1076 const A65: f64 = -5103.0 / 18656.0;
1077 const A71: f64 = 35.0 / 384.0;
1078 const A73: f64 = 500.0 / 1113.0;
1079 const A74: f64 = 125.0 / 192.0;
1080 const A75: f64 = -2187.0 / 6784.0;
1081 const A76: f64 = 11.0 / 84.0;
1082
1083 const B1: f64 = 35.0 / 384.0;
1085 const B3: f64 = 500.0 / 1113.0;
1086 const B4: f64 = 125.0 / 192.0;
1087 const B5: f64 = -2187.0 / 6784.0;
1088 const B6: f64 = 11.0 / 84.0;
1089
1090 const B1_ERR: f64 = 5179.0 / 57600.0;
1092 const B3_ERR: f64 = 7571.0 / 16695.0;
1093 const B4_ERR: f64 = 393.0 / 640.0;
1094 const B5_ERR: f64 = -92097.0 / 339200.0;
1095 const B6_ERR: f64 = 187.0 / 2100.0;
1096 const B7_ERR: f64 = 1.0 / 40.0;
1097
1098 let k1_v = self.calculate_acceleration(position, velocity, air_density, wind_vector);
1100 let k1_p = *velocity;
1101
1102 let p2 = position + dt * A21 * k1_p;
1103 let v2 = velocity + dt * A21 * k1_v;
1104 let k2_v = self.calculate_acceleration(&p2, &v2, air_density, wind_vector);
1105 let k2_p = v2;
1106
1107 let p3 = position + dt * (A31 * k1_p + A32 * k2_p);
1108 let v3 = velocity + dt * (A31 * k1_v + A32 * k2_v);
1109 let k3_v = self.calculate_acceleration(&p3, &v3, air_density, wind_vector);
1110 let k3_p = v3;
1111
1112 let p4 = position + dt * (A41 * k1_p + A42 * k2_p + A43 * k3_p);
1113 let v4 = velocity + dt * (A41 * k1_v + A42 * k2_v + A43 * k3_v);
1114 let k4_v = self.calculate_acceleration(&p4, &v4, air_density, wind_vector);
1115 let k4_p = v4;
1116
1117 let p5 = position + dt * (A51 * k1_p + A52 * k2_p + A53 * k3_p + A54 * k4_p);
1118 let v5 = velocity + dt * (A51 * k1_v + A52 * k2_v + A53 * k3_v + A54 * k4_v);
1119 let k5_v = self.calculate_acceleration(&p5, &v5, air_density, wind_vector);
1120 let k5_p = v5;
1121
1122 let p6 = position + dt * (A61 * k1_p + A62 * k2_p + A63 * k3_p + A64 * k4_p + A65 * k5_p);
1123 let v6 = velocity + dt * (A61 * k1_v + A62 * k2_v + A63 * k3_v + A64 * k4_v + A65 * k5_v);
1124 let k6_v = self.calculate_acceleration(&p6, &v6, air_density, wind_vector);
1125 let k6_p = v6;
1126
1127 let p7 = position + dt * (A71 * k1_p + A73 * k3_p + A74 * k4_p + A75 * k5_p + A76 * k6_p);
1128 let v7 = velocity + dt * (A71 * k1_v + A73 * k3_v + A74 * k4_v + A75 * k5_v + A76 * k6_v);
1129 let k7_v = self.calculate_acceleration(&p7, &v7, air_density, wind_vector);
1130 let k7_p = v7;
1131
1132 let new_pos = position + dt * (B1 * k1_p + B3 * k3_p + B4 * k4_p + B5 * k5_p + B6 * k6_p);
1134 let new_vel = velocity + dt * (B1 * k1_v + B3 * k3_v + B4 * k4_v + B5 * k5_v + B6 * k6_v);
1135
1136 let pos_err = position
1138 + dt * (B1_ERR * k1_p
1139 + B3_ERR * k3_p
1140 + B4_ERR * k4_p
1141 + B5_ERR * k5_p
1142 + B6_ERR * k6_p
1143 + B7_ERR * k7_p);
1144 let vel_err = velocity
1145 + dt * (B1_ERR * k1_v
1146 + B3_ERR * k3_v
1147 + B4_ERR * k4_v
1148 + B5_ERR * k5_v
1149 + B6_ERR * k6_v
1150 + B7_ERR * k7_v);
1151
1152 let pos_error = (new_pos - pos_err).magnitude();
1154 let vel_error = (new_vel - vel_err).magnitude();
1155 let error = (pos_error + vel_error) / (1.0 + position.magnitude() + velocity.magnitude());
1156
1157 let dt_new = if error < tolerance {
1159 dt * (tolerance / error).powf(0.2).min(2.0)
1160 } else {
1161 dt * (tolerance / error).powf(0.25).max(0.1)
1162 };
1163
1164 (new_pos, new_vel, dt_new)
1165 }
1166
1167 fn calculate_acceleration(
1168 &self,
1169 position: &Vector3<f64>,
1170 velocity: &Vector3<f64>,
1171 air_density: f64,
1172 wind_vector: &Vector3<f64>,
1173 ) -> Vector3<f64> {
1174 let actual_wind = if self.inputs.enable_wind_shear {
1176 self.get_wind_at_altitude(position.y)
1177 } else {
1178 *wind_vector
1179 };
1180
1181 let relative_velocity = velocity - actual_wind;
1182 let velocity_magnitude = relative_velocity.magnitude();
1183
1184 if velocity_magnitude < 0.001 {
1185 return Vector3::new(0.0, -9.81, 0.0);
1186 }
1187
1188 let cd = self.calculate_drag_coefficient(velocity_magnitude);
1190
1191 let velocity_fps = velocity_magnitude * 3.28084;
1193
1194 let base_bc = if let Some(ref segments) = self.inputs.bc_segments_data {
1196 segments
1198 .iter()
1199 .find(|seg| velocity_fps >= seg.velocity_min && velocity_fps < seg.velocity_max)
1200 .map(|seg| seg.bc_value)
1201 .unwrap_or(self.inputs.bc_value)
1202 } else {
1203 self.inputs.bc_value
1204 };
1205
1206 let effective_bc = if let Some(ref cluster_bc) = self.cluster_bc {
1208 cluster_bc.apply_correction(
1209 base_bc,
1210 self.inputs.caliber_inches * 0.0254, self.inputs.weight_grains,
1212 velocity_fps,
1213 )
1214 } else {
1215 base_bc
1216 };
1217
1218 let cd_to_retard = 0.000683 * 0.30; let standard_factor = cd * cd_to_retard;
1224 let density_scale = air_density / 1.225; let a_drag_ft_s2 =
1228 (velocity_fps * velocity_fps) * standard_factor * density_scale / effective_bc;
1229 let a_drag_m_s2 = a_drag_ft_s2 * 0.3048; let drag_acceleration = -a_drag_m_s2 * (relative_velocity / velocity_magnitude);
1233
1234 drag_acceleration + Vector3::new(0.0, -9.81, 0.0)
1236 }
1237
1238 fn calculate_drag_coefficient(&self, velocity: f64) -> f64 {
1239 let temp_c = self.atmosphere.temperature;
1241 let temp_k = temp_c + 273.15;
1242 let gamma = 1.4; let r_specific = 287.05; let speed_of_sound = (gamma * r_specific * temp_k).sqrt();
1245 let mach = velocity / speed_of_sound;
1246
1247 let base_cd = crate::drag::get_drag_coefficient(mach, &self.inputs.bc_type);
1249
1250 let projectile_shape = if let Some(ref model) = self.inputs.bullet_model {
1252 if model.to_lowercase().contains("boat") || model.to_lowercase().contains("bt") {
1254 ProjectileShape::BoatTail
1255 } else if model.to_lowercase().contains("round") || model.to_lowercase().contains("rn")
1256 {
1257 ProjectileShape::RoundNose
1258 } else if model.to_lowercase().contains("flat") || model.to_lowercase().contains("fb") {
1259 ProjectileShape::FlatBase
1260 } else {
1261 get_projectile_shape(
1263 self.inputs.bullet_diameter,
1264 self.inputs.bullet_mass / 0.00006479891, &self.inputs.bc_type.to_string(),
1266 )
1267 }
1268 } else {
1269 get_projectile_shape(
1271 self.inputs.bullet_diameter,
1272 self.inputs.bullet_mass / 0.00006479891, &self.inputs.bc_type.to_string(),
1274 )
1275 };
1276
1277 let include_wave_drag = false;
1283 transonic_correction(mach, base_cd, projectile_shape, include_wave_drag)
1284 }
1285}
1286
1287#[derive(Debug, Clone)]
1289pub struct MonteCarloParams {
1290 pub num_simulations: usize,
1291 pub velocity_std_dev: f64,
1292 pub angle_std_dev: f64,
1293 pub bc_std_dev: f64,
1294 pub wind_speed_std_dev: f64,
1295 pub target_distance: Option<f64>,
1296 pub base_wind_speed: f64,
1297 pub base_wind_direction: f64,
1298 pub azimuth_std_dev: f64, }
1300
1301impl Default for MonteCarloParams {
1302 fn default() -> Self {
1303 Self {
1304 num_simulations: 1000,
1305 velocity_std_dev: 1.0,
1306 angle_std_dev: 0.001,
1307 bc_std_dev: 0.01,
1308 wind_speed_std_dev: 1.0,
1309 target_distance: None,
1310 base_wind_speed: 0.0,
1311 base_wind_direction: 0.0,
1312 azimuth_std_dev: 0.001, }
1314 }
1315}
1316
1317#[derive(Debug, Clone)]
1319pub struct MonteCarloResults {
1320 pub ranges: Vec<f64>,
1321 pub impact_velocities: Vec<f64>,
1322 pub impact_positions: Vec<Vector3<f64>>,
1323}
1324
1325pub fn run_monte_carlo(
1327 base_inputs: BallisticInputs,
1328 params: MonteCarloParams,
1329) -> Result<MonteCarloResults, BallisticsError> {
1330 let base_wind = WindConditions {
1331 speed: params.base_wind_speed,
1332 direction: params.base_wind_direction,
1333 };
1334 run_monte_carlo_with_wind(base_inputs, base_wind, params)
1335}
1336
1337pub fn run_monte_carlo_with_wind(
1339 base_inputs: BallisticInputs,
1340 base_wind: WindConditions,
1341 params: MonteCarloParams,
1342) -> Result<MonteCarloResults, BallisticsError> {
1343 use rand::thread_rng;
1344 use rand_distr::{Distribution, Normal};
1345
1346 let mut rng = thread_rng();
1347 let mut ranges = Vec::new();
1348 let mut impact_velocities = Vec::new();
1349 let mut impact_positions = Vec::new();
1350
1351 let baseline_solver =
1353 TrajectorySolver::new(base_inputs.clone(), base_wind.clone(), Default::default());
1354 let baseline_result = baseline_solver.solve()?;
1355
1356 let target_distance = params.target_distance.unwrap_or(baseline_result.max_range);
1358
1359 let baseline_at_target = baseline_result
1361 .position_at_range(target_distance)
1362 .ok_or("Could not interpolate baseline at target distance")?;
1363
1364 let velocity_dist = Normal::new(base_inputs.muzzle_velocity, params.velocity_std_dev)
1366 .map_err(|e| format!("Invalid velocity distribution: {}", e))?;
1367 let angle_dist = Normal::new(base_inputs.muzzle_angle, params.angle_std_dev)
1368 .map_err(|e| format!("Invalid angle distribution: {}", e))?;
1369 let bc_dist = Normal::new(base_inputs.bc_value, params.bc_std_dev)
1370 .map_err(|e| format!("Invalid BC distribution: {}", e))?;
1371 let wind_speed_dist = Normal::new(base_wind.speed, params.wind_speed_std_dev)
1372 .map_err(|e| format!("Invalid wind speed distribution: {}", e))?;
1373 let wind_dir_dist =
1374 Normal::new(base_wind.direction, params.wind_speed_std_dev * 0.1) .map_err(|e| format!("Invalid wind direction distribution: {}", e))?;
1376 let azimuth_dist = Normal::new(base_inputs.azimuth_angle, params.azimuth_std_dev)
1377 .map_err(|e| format!("Invalid azimuth distribution: {}", e))?;
1378
1379 let pointing_error_dist = Normal::new(0.0, params.angle_std_dev * target_distance)
1381 .map_err(|e| format!("Invalid pointing distribution: {}", e))?;
1382
1383 for _ in 0..params.num_simulations {
1384 let mut inputs = base_inputs.clone();
1386 inputs.muzzle_velocity = velocity_dist.sample(&mut rng).max(0.0);
1387 inputs.muzzle_angle = angle_dist.sample(&mut rng);
1388 inputs.bc_value = bc_dist.sample(&mut rng).max(0.01);
1389 inputs.azimuth_angle = azimuth_dist.sample(&mut rng); let wind = WindConditions {
1393 speed: wind_speed_dist.sample(&mut rng).abs(),
1394 direction: wind_dir_dist.sample(&mut rng),
1395 };
1396
1397 let solver = TrajectorySolver::new(inputs, wind, Default::default());
1399 match solver.solve() {
1400 Ok(result) => {
1401 ranges.push(result.max_range);
1402 impact_velocities.push(result.impact_velocity);
1403
1404 if let Some(pos_at_target) = result.position_at_range(target_distance) {
1406 let mut deviation = Vector3::new(
1409 pos_at_target.x - baseline_at_target.x, pos_at_target.y - baseline_at_target.y, 0.0, );
1413
1414 let pointing_error_y = pointing_error_dist.sample(&mut rng);
1417 deviation.y += pointing_error_y;
1418
1419 impact_positions.push(deviation);
1420 }
1421 }
1422 Err(_) => {
1423 continue;
1425 }
1426 }
1427 }
1428
1429 if ranges.is_empty() {
1430 return Err("No successful simulations".into());
1431 }
1432
1433 Ok(MonteCarloResults {
1434 ranges,
1435 impact_velocities,
1436 impact_positions,
1437 })
1438}
1439
1440pub fn calculate_zero_angle(
1442 inputs: BallisticInputs,
1443 target_distance: f64,
1444 target_height: f64,
1445) -> Result<f64, BallisticsError> {
1446 calculate_zero_angle_with_conditions(
1447 inputs,
1448 target_distance,
1449 target_height,
1450 WindConditions::default(),
1451 AtmosphericConditions::default(),
1452 )
1453}
1454
1455pub fn calculate_zero_angle_with_conditions(
1456 inputs: BallisticInputs,
1457 target_distance: f64,
1458 target_height: f64,
1459 wind: WindConditions,
1460 atmosphere: AtmosphericConditions,
1461) -> Result<f64, BallisticsError> {
1462 let get_height_at_angle = |angle: f64| -> Result<Option<f64>, BallisticsError> {
1464 let mut test_inputs = inputs.clone();
1465 test_inputs.muzzle_angle = angle;
1466
1467 let mut solver = TrajectorySolver::new(test_inputs, wind.clone(), atmosphere.clone());
1468 solver.set_max_range(target_distance * 2.0);
1469 solver.set_time_step(0.001);
1470 let result = solver.solve()?;
1471
1472 for i in 0..result.points.len() {
1474 if result.points[i].position.z >= target_distance {
1475 if i > 0 {
1476 let p1 = &result.points[i - 1];
1477 let p2 = &result.points[i];
1478 let t = (target_distance - p1.position.z) / (p2.position.z - p1.position.z);
1479 return Ok(Some(p1.position.y + t * (p2.position.y - p1.position.y)));
1480 } else {
1481 return Ok(Some(result.points[i].position.y));
1482 }
1483 }
1484 }
1485 Ok(None)
1486 };
1487
1488 let mut low_angle = 0.0; let mut high_angle = 0.2; let tolerance = 0.00001; let max_iterations = 50;
1494
1495 let low_height = get_height_at_angle(low_angle)?;
1498 let high_height = get_height_at_angle(high_angle)?;
1499
1500 match (low_height, high_height) {
1501 (Some(lh), Some(hh)) => {
1502 let low_error = lh - target_height;
1503 let high_error = hh - target_height;
1504
1505 if low_error > 0.0 && high_error > 0.0 {
1508 } else if low_error < 0.0 && high_error < 0.0 {
1513 let mut expanded = false;
1516 for multiplier in [2.0, 3.0, 4.0] {
1517 let new_high = (high_angle * multiplier).min(0.785);
1518 if let Ok(Some(h)) = get_height_at_angle(new_high) {
1519 if h - target_height > 0.0 {
1520 high_angle = new_high;
1521 expanded = true;
1522 break;
1523 }
1524 }
1525 if new_high >= 0.785 {
1526 break;
1527 }
1528 }
1529 if !expanded {
1530 return Err("Cannot find zero angle: target beyond effective range even at maximum angle".into());
1531 }
1532 }
1533 }
1535 (None, Some(_hh)) => {
1536 }
1539 (Some(_lh), None) => {
1540 return Err(
1542 "Cannot find zero angle: high angle trajectory doesn't reach target distance"
1543 .into(),
1544 );
1545 }
1546 (None, None) => {
1547 return Err(
1549 "Cannot find zero angle: trajectory cannot reach target distance at any angle"
1550 .into(),
1551 );
1552 }
1553 }
1554
1555 for _iteration in 0..max_iterations {
1556 let mid_angle = (low_angle + high_angle) / 2.0;
1557
1558 let mut test_inputs = inputs.clone();
1559 test_inputs.muzzle_angle = mid_angle;
1560
1561 let mut solver = TrajectorySolver::new(test_inputs, wind.clone(), atmosphere.clone());
1562 solver.set_max_range(target_distance * 2.0);
1564 solver.set_time_step(0.001);
1565 let result = solver.solve()?;
1566
1567 let mut height_at_target = None;
1569 for i in 0..result.points.len() {
1570 if result.points[i].position.z >= target_distance {
1571 if i > 0 {
1572 let p1 = &result.points[i - 1];
1574 let p2 = &result.points[i];
1575 let t = (target_distance - p1.position.z) / (p2.position.z - p1.position.z);
1576 height_at_target = Some(p1.position.y + t * (p2.position.y - p1.position.y));
1577 } else {
1578 height_at_target = Some(result.points[i].position.y);
1579 }
1580 break;
1581 }
1582 }
1583
1584 match height_at_target {
1585 Some(height) => {
1586 let error = height - target_height;
1587 if error.abs() < 0.001 {
1590 return Ok(mid_angle);
1591 }
1592
1593 if (high_angle - low_angle).abs() < tolerance {
1597 if error.abs() < 0.01 {
1598 return Ok(mid_angle);
1600 }
1601 return Ok(mid_angle);
1604 }
1605
1606 if error > 0.0 {
1607 high_angle = mid_angle;
1608 } else {
1609 low_angle = mid_angle;
1610 }
1611 }
1612 None => {
1613 low_angle = mid_angle;
1615
1616 if (high_angle - low_angle).abs() < tolerance {
1618 return Err("Trajectory cannot reach target distance - angle converged without valid solution".into());
1619 }
1620 }
1621 }
1622 }
1623
1624 Err("Failed to find zero angle".into())
1625}
1626
1627pub fn estimate_bc_from_trajectory(
1629 velocity: f64,
1630 mass: f64,
1631 diameter: f64,
1632 points: &[(f64, f64)], ) -> Result<f64, BallisticsError> {
1634 let mut best_bc = 0.5;
1636 let mut best_error = f64::MAX;
1637 let mut found_valid = false;
1638
1639 for bc in (100..1000).step_by(10) {
1641 let bc_value = bc as f64 / 1000.0;
1642
1643 let inputs = BallisticInputs {
1644 muzzle_velocity: velocity,
1645 bc_value,
1646 bullet_mass: mass,
1647 bullet_diameter: diameter,
1648 ..Default::default()
1649 };
1650
1651 let mut solver = TrajectorySolver::new(inputs, Default::default(), Default::default());
1652 solver.set_max_range(points.last().map(|(d, _)| *d * 1.5).unwrap_or(1000.0));
1654
1655 let result = match solver.solve() {
1656 Ok(r) => r,
1657 Err(_) => continue, };
1659
1660 let mut total_error = 0.0;
1662 for (target_dist, target_drop) in points {
1663 let mut calculated_drop = None;
1665 for i in 0..result.points.len() {
1666 if result.points[i].position.z >= *target_dist {
1667 if i > 0 {
1668 let p1 = &result.points[i - 1];
1670 let p2 = &result.points[i];
1671 let t = (target_dist - p1.position.z) / (p2.position.z - p1.position.z);
1672 calculated_drop =
1673 Some(-(p1.position.y + t * (p2.position.y - p1.position.y)));
1674 } else {
1675 calculated_drop = Some(-result.points[i].position.y);
1676 }
1677 break;
1678 }
1679 }
1680
1681 if let Some(drop) = calculated_drop {
1682 let error = (drop - target_drop).abs();
1683 total_error += error * error;
1684 }
1685 }
1686
1687 if total_error < best_error {
1688 best_error = total_error;
1689 best_bc = bc_value;
1690 found_valid = true;
1691 }
1692 }
1693
1694 if !found_valid {
1695 return Err(BallisticsError::from("Unable to estimate BC from provided data. Check that drop values are in correct units.".to_string()));
1696 }
1697
1698 Ok(best_bc)
1699}
1700
1701fn calculate_air_density(atmosphere: &AtmosphericConditions) -> f64 {
1703 let r_specific = 287.058; let temperature_k = atmosphere.temperature + 273.15;
1707
1708 let pressure_pa = atmosphere.pressure * 100.0;
1710
1711 let density = pressure_pa / (r_specific * temperature_k);
1713
1714 let altitude_factor = (-atmosphere.altitude / 8000.0).exp();
1716
1717 density * altitude_factor
1718}
1719
1720use rand;
1722use rand_distr;