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 enable_magnus: bool, pub enable_coriolis: bool, pub use_powder_sensitivity: bool,
113 pub powder_temp_sensitivity: f64,
114 pub powder_temp: f64, pub tipoff_yaw: f64, pub tipoff_decay_distance: f64, pub use_bc_segments: bool,
118 pub bc_segments: Option<Vec<(f64, f64)>>, pub bc_segments_data: Option<Vec<crate::BCSegmentData>>, pub use_enhanced_spin_drift: bool,
121 pub use_form_factor: bool,
122 pub enable_wind_shear: bool,
123 pub wind_shear_model: String,
124 pub enable_trajectory_sampling: bool,
125 pub sample_interval: f64, pub enable_pitch_damping: bool,
127 pub enable_precession_nutation: bool,
128 pub use_cluster_bc: bool, pub custom_drag_table: Option<crate::drag::DragTable>,
132
133 pub bc_type_str: Option<String>,
135}
136
137impl Default for BallisticInputs {
138 fn default() -> Self {
139 let mass_kg = 0.01;
140 let diameter_m = 0.00762;
141 let bc = 0.5;
142 let muzzle_angle_rad = 0.0;
143 let bc_type = DragModel::G1;
144
145 Self {
146 bc_value: bc,
148 bc_type,
149 bullet_mass: mass_kg,
150 muzzle_velocity: 800.0,
151 bullet_diameter: diameter_m,
152 bullet_length: diameter_m * 4.0, muzzle_angle: muzzle_angle_rad,
156 target_distance: 100.0,
157 azimuth_angle: 0.0,
158 shooting_angle: 0.0,
159 sight_height: 0.05,
160 muzzle_height: 0.0, target_height: 0.0, ground_threshold: -100.0, altitude: 0.0,
166 temperature: 15.0,
167 pressure: 1013.25, humidity: 0.5, latitude: None,
170
171 wind_speed: 0.0,
173 wind_angle: 0.0,
174
175 twist_rate: 12.0, is_twist_right: true,
178 caliber_inches: diameter_m / 0.0254, weight_grains: mass_kg / 0.00006479891, manufacturer: None,
181 bullet_model: None,
182 bullet_id: None,
183 bullet_cluster: None,
184
185 use_rk4: true, use_adaptive_rk45: true, enable_advanced_effects: false,
191 enable_magnus: false,
192 enable_coriolis: false,
193 use_powder_sensitivity: false,
194 powder_temp_sensitivity: 0.0,
195 powder_temp: 15.0,
196 tipoff_yaw: 0.0,
197 tipoff_decay_distance: 50.0,
198 use_bc_segments: false,
199 bc_segments: None,
200 bc_segments_data: None,
201 use_enhanced_spin_drift: false,
202 use_form_factor: false,
203 enable_wind_shear: false,
204 wind_shear_model: "none".to_string(),
205 enable_trajectory_sampling: false,
206 sample_interval: 10.0, enable_pitch_damping: false,
208 enable_precession_nutation: false,
209 use_cluster_bc: false, custom_drag_table: None,
213
214 bc_type_str: None,
216 }
217 }
218}
219
220#[derive(Debug, Clone)]
222pub struct WindConditions {
223 pub speed: f64, pub direction: f64, }
226
227impl Default for WindConditions {
228 fn default() -> Self {
229 Self {
230 speed: 0.0,
231 direction: 0.0,
232 }
233 }
234}
235
236#[derive(Debug, Clone)]
238pub struct AtmosphericConditions {
239 pub temperature: f64, pub pressure: f64, pub humidity: f64, pub altitude: f64, }
244
245impl Default for AtmosphericConditions {
246 fn default() -> Self {
247 Self {
248 temperature: 15.0,
249 pressure: 1013.25,
250 humidity: 50.0,
251 altitude: 0.0,
252 }
253 }
254}
255
256#[derive(Debug, Clone)]
258pub struct TrajectoryPoint {
259 pub time: f64,
260 pub position: Vector3<f64>,
261 pub velocity_magnitude: f64,
262 pub kinetic_energy: f64,
263}
264
265#[derive(Debug, Clone)]
267pub struct TrajectoryResult {
268 pub max_range: f64,
269 pub max_height: f64,
270 pub time_of_flight: f64,
271 pub impact_velocity: f64,
272 pub impact_energy: f64,
273 pub points: Vec<TrajectoryPoint>,
274 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>, }
281
282impl TrajectoryResult {
283 pub fn position_at_range(&self, target_range: f64) -> Option<Vector3<f64>> {
287 if self.points.is_empty() {
288 return None;
289 }
290
291 for i in 0..self.points.len() - 1 {
293 let p1 = &self.points[i];
294 let p2 = &self.points[i + 1];
295
296 if p1.position.x <= target_range && p2.position.x >= target_range {
298 let dx = p2.position.x - p1.position.x;
300 if dx.abs() < 1e-10 {
301 return Some(p1.position);
302 }
303 let t = (target_range - p1.position.x) / dx;
304
305 return Some(Vector3::new(
307 target_range,
308 p1.position.y + t * (p2.position.y - p1.position.y),
309 p1.position.z + t * (p2.position.z - p1.position.z),
310 ));
311 }
312 }
313
314 self.points.last().map(|p| p.position)
316 }
317}
318
319pub struct TrajectorySolver {
321 inputs: BallisticInputs,
322 wind: WindConditions,
323 atmosphere: AtmosphericConditions,
324 max_range: f64,
325 time_step: f64,
326 cluster_bc: Option<ClusterBCDegradation>,
327}
328
329impl TrajectorySolver {
330 pub fn new(
331 mut inputs: BallisticInputs,
332 wind: WindConditions,
333 atmosphere: AtmosphericConditions,
334 ) -> Self {
335 inputs.caliber_inches = inputs.bullet_diameter / 0.0254;
337 inputs.weight_grains = inputs.bullet_mass / 0.00006479891;
338
339 let cluster_bc = if inputs.use_cluster_bc {
341 Some(ClusterBCDegradation::new())
342 } else {
343 None
344 };
345
346 Self {
347 inputs,
348 wind,
349 atmosphere,
350 max_range: 1000.0,
351 time_step: 0.001,
352 cluster_bc,
353 }
354 }
355
356 pub fn set_max_range(&mut self, range: f64) {
357 self.max_range = range;
358 }
359
360 pub fn set_time_step(&mut self, step: f64) {
361 self.time_step = step;
362 }
363
364 fn get_wind_at_altitude(&self, altitude_m: f64) -> Vector3<f64> {
365 let profile = WindShearProfile {
367 model: if self.inputs.wind_shear_model == "logarithmic" {
368 WindShearModel::Logarithmic
369 } else if self.inputs.wind_shear_model == "power" {
370 WindShearModel::PowerLaw
371 } else {
372 WindShearModel::PowerLaw },
374 surface_wind: WindLayer {
375 altitude_m: 0.0,
376 speed_mps: self.wind.speed,
377 direction_deg: self.wind.direction.to_degrees(),
378 },
379 reference_height: 10.0, roughness_length: 0.03, power_exponent: 1.0 / 7.0, geostrophic_wind: None,
383 custom_layers: Vec::new(),
384 };
385
386 profile.get_wind_at_altitude(altitude_m)
387 }
388
389 pub fn solve(&self) -> Result<TrajectoryResult, BallisticsError> {
390 let mut result = if self.inputs.use_rk4 {
391 if self.inputs.use_adaptive_rk45 {
392 self.solve_rk45()?
393 } else {
394 self.solve_rk4()?
395 }
396 } else {
397 self.solve_euler()?
398 };
399 self.apply_spin_drift(&mut result);
400 Ok(result)
401 }
402
403 fn apply_spin_drift(&self, result: &mut TrajectoryResult) {
409 if !self.inputs.use_enhanced_spin_drift {
410 return;
411 }
412 let d_in = self.inputs.bullet_diameter / 0.0254; let m_gr = self.inputs.bullet_mass / 0.00006479891; let twist_in = self.inputs.twist_rate; if d_in <= 0.0 || m_gr <= 0.0 || twist_in <= 0.0 {
416 return;
417 }
418
419 let length_in = if self.inputs.bullet_length > 0.0 {
421 self.inputs.bullet_length / 0.0254
422 } else {
423 4.5 * d_in
424 };
425 let sg = crate::spin_drift::miller_stability(d_in, m_gr, twist_in, length_in);
426 let sign = if self.inputs.is_twist_right { 1.0 } else { -1.0 };
427
428 for p in result.points.iter_mut() {
429 if p.time <= 0.0 {
430 continue;
431 }
432 let sd_in = 1.25 * (sg + 1.2) * p.time.powf(1.83); p.position.z += sign * sd_in * 0.0254; }
435 }
436
437 fn solve_euler(&self) -> Result<TrajectoryResult, BallisticsError> {
438 let mut time = 0.0;
440 let mut position = Vector3::new(
443 0.0,
444 self.inputs.muzzle_height, 0.0,
446 );
447 let horizontal_velocity = self.inputs.muzzle_velocity * self.inputs.muzzle_angle.cos();
450 let mut velocity = Vector3::new(
451 horizontal_velocity * self.inputs.azimuth_angle.cos(), self.inputs.muzzle_velocity * self.inputs.muzzle_angle.sin(), horizontal_velocity * self.inputs.azimuth_angle.sin(), );
455
456 let mut points = Vec::new();
457 let mut max_height = position.y;
458 let mut min_pitch_damping = 1.0; let mut transonic_mach = None; let mut angular_state = if self.inputs.enable_precession_nutation {
463 Some(AngularState {
464 pitch_angle: 0.001, yaw_angle: 0.001,
466 pitch_rate: 0.0,
467 yaw_rate: 0.0,
468 precession_angle: 0.0,
469 nutation_phase: 0.0,
470 })
471 } else {
472 None
473 };
474 let mut max_yaw_angle = 0.0;
475 let mut max_precession_angle = 0.0;
476
477 let air_density = calculate_air_density(&self.atmosphere);
479
480 let wind_vector = Vector3::new(
482 self.wind.speed * self.wind.direction.cos(), 0.0,
484 self.wind.speed * self.wind.direction.sin(), );
486
487 while position.x < self.max_range && position.y >= 0.0 && time < 100.0 {
489 let velocity_magnitude = velocity.magnitude();
491 let kinetic_energy =
492 0.5 * self.inputs.bullet_mass * velocity_magnitude * velocity_magnitude;
493
494 points.push(TrajectoryPoint {
495 time,
496 position: position,
497 velocity_magnitude,
498 kinetic_energy,
499 });
500
501 if points.len() == 1 || points.len() % 100 == 0 {
504 eprintln!("Trajectory point {}: time={:.3}s, downrange={:.2}m, vertical={:.2}m, lateral={:.2}m, vel={:.1}m/s",
505 points.len(), time, position.x, position.y, position.z, velocity_magnitude);
506 }
507
508 if position.y > max_height {
510 max_height = position.y;
511 }
512
513 if self.inputs.enable_pitch_damping {
515 let temp_c = self.atmosphere.temperature;
516 let temp_k = temp_c + 273.15;
517 let speed_of_sound = (1.4 * 287.05 * temp_k).sqrt();
518 let mach = velocity_magnitude / speed_of_sound;
519
520 if transonic_mach.is_none() && mach < 1.2 && mach > 0.8 {
522 transonic_mach = Some(mach);
523 }
524
525 let bullet_type = if let Some(ref model) = self.inputs.bullet_model {
527 model.as_str()
528 } else {
529 "default"
530 };
531 let coeffs = PitchDampingCoefficients::from_bullet_type(bullet_type);
532 let pitch_damping = calculate_pitch_damping_coefficient(mach, &coeffs);
533
534 if pitch_damping < min_pitch_damping {
536 min_pitch_damping = pitch_damping;
537 }
538 }
539
540 if self.inputs.enable_precession_nutation {
542 if let Some(ref mut state) = angular_state {
543 let velocity_magnitude = velocity.magnitude();
544 let temp_c = self.atmosphere.temperature;
545 let temp_k = temp_c + 273.15;
546 let speed_of_sound = (1.4 * 287.05 * temp_k).sqrt();
547 let mach = velocity_magnitude / speed_of_sound;
548
549 let spin_rate_rad_s = if self.inputs.twist_rate > 0.0 {
551 let velocity_fps = velocity_magnitude * 3.28084;
552 let twist_rate_ft = self.inputs.twist_rate / 12.0;
553 (velocity_fps / twist_rate_ft) * 2.0 * std::f64::consts::PI
554 } else {
555 0.0
556 };
557
558 let params = PrecessionNutationParams {
560 mass_kg: self.inputs.bullet_mass,
561 caliber_m: self.inputs.bullet_diameter,
562 length_m: self.inputs.bullet_length,
563 spin_rate_rad_s,
564 spin_inertia: 6.94e-8, transverse_inertia: 9.13e-7, velocity_mps: velocity_magnitude,
567 air_density_kg_m3: air_density,
568 mach,
569 pitch_damping_coeff: -0.8,
570 nutation_damping_factor: 0.05,
571 };
572
573 *state = calculate_combined_angular_motion(
575 ¶ms,
576 state,
577 time,
578 self.time_step,
579 0.001, );
581
582 if state.yaw_angle.abs() > max_yaw_angle {
584 max_yaw_angle = state.yaw_angle.abs();
585 }
586 if state.precession_angle.abs() > max_precession_angle {
587 max_precession_angle = state.precession_angle.abs();
588 }
589 }
590 }
591
592 let actual_wind = if self.inputs.enable_wind_shear {
594 self.get_wind_at_altitude(position.y)
595 } else {
596 wind_vector
597 };
598 let velocity_rel = velocity - actual_wind;
599 let velocity_rel_mag = velocity_rel.magnitude();
600 let drag_coefficient = self.calculate_drag_coefficient(velocity_rel_mag);
601
602 let drag_force = 0.5
604 * air_density
605 * drag_coefficient
606 * self.inputs.bullet_diameter
607 * self.inputs.bullet_diameter
608 * std::f64::consts::PI
609 / 4.0
610 * velocity_rel_mag
611 * velocity_rel_mag;
612
613 let drag_acceleration = -drag_force / self.inputs.bullet_mass;
615 let acceleration = Vector3::new(
616 drag_acceleration * velocity_rel.x / velocity_rel_mag,
617 drag_acceleration * velocity_rel.y / velocity_rel_mag - 9.80665,
618 drag_acceleration * velocity_rel.z / velocity_rel_mag,
619 );
620
621 velocity += acceleration * self.time_step;
623 position += velocity * self.time_step;
624 time += self.time_step;
625 }
626
627 let last_point = points.last().ok_or("No trajectory points generated")?;
629
630 let sampled_points = if self.inputs.enable_trajectory_sampling {
632 let trajectory_data = TrajectoryData {
633 times: points.iter().map(|p| p.time).collect(),
634 positions: points.iter().map(|p| p.position).collect(),
635 velocities: points
636 .iter()
637 .map(|p| {
638 Vector3::new(0.0, 0.0, p.velocity_magnitude)
640 })
641 .collect(),
642 transonic_distances: Vec::new(), };
644
645 let sight_position_m = self.inputs.muzzle_height + self.inputs.sight_height;
650 let outputs = TrajectoryOutputs {
651 target_distance_horiz_m: last_point.position.x, target_vertical_height_m: sight_position_m,
653 time_of_flight_s: last_point.time,
654 max_ord_dist_horiz_m: max_height,
655 sight_height_m: sight_position_m,
656 };
657
658 let samples = sample_trajectory(
660 &trajectory_data,
661 &outputs,
662 self.inputs.sample_interval,
663 self.inputs.bullet_mass,
664 );
665 Some(samples)
666 } else {
667 None
668 };
669
670 Ok(TrajectoryResult {
671 max_range: last_point.position.x, max_height,
673 time_of_flight: last_point.time,
674 impact_velocity: last_point.velocity_magnitude,
675 impact_energy: last_point.kinetic_energy,
676 points,
677 sampled_points,
678 min_pitch_damping: if self.inputs.enable_pitch_damping {
679 Some(min_pitch_damping)
680 } else {
681 None
682 },
683 transonic_mach,
684 angular_state,
685 max_yaw_angle: if self.inputs.enable_precession_nutation {
686 Some(max_yaw_angle)
687 } else {
688 None
689 },
690 max_precession_angle: if self.inputs.enable_precession_nutation {
691 Some(max_precession_angle)
692 } else {
693 None
694 },
695 })
696 }
697
698 fn solve_rk4(&self) -> Result<TrajectoryResult, BallisticsError> {
699 let mut time = 0.0;
701 let mut position = Vector3::new(
705 0.0,
706 self.inputs.muzzle_height, 0.0,
708 );
709
710 let horizontal_velocity = self.inputs.muzzle_velocity * self.inputs.muzzle_angle.cos();
713 let mut velocity = Vector3::new(
714 horizontal_velocity * self.inputs.azimuth_angle.cos(), self.inputs.muzzle_velocity * self.inputs.muzzle_angle.sin(), horizontal_velocity * self.inputs.azimuth_angle.sin(), );
718
719 let mut points = Vec::new();
720 let mut max_height = position.y;
721 let mut min_pitch_damping = 1.0; let mut transonic_mach = None; let mut angular_state = if self.inputs.enable_precession_nutation {
726 Some(AngularState {
727 pitch_angle: 0.001, yaw_angle: 0.001,
729 pitch_rate: 0.0,
730 yaw_rate: 0.0,
731 precession_angle: 0.0,
732 nutation_phase: 0.0,
733 })
734 } else {
735 None
736 };
737 let mut max_yaw_angle = 0.0;
738 let mut max_precession_angle = 0.0;
739
740 let air_density = calculate_air_density(&self.atmosphere);
742
743 let wind_vector = Vector3::new(
745 self.wind.speed * self.wind.direction.cos(), 0.0,
747 self.wind.speed * self.wind.direction.sin(), );
749
750 while position.x < self.max_range && position.y >= 0.0 && time < 100.0 {
752 let velocity_magnitude = velocity.magnitude();
754 let kinetic_energy =
755 0.5 * self.inputs.bullet_mass * velocity_magnitude * velocity_magnitude;
756
757 points.push(TrajectoryPoint {
758 time,
759 position: position,
760 velocity_magnitude,
761 kinetic_energy,
762 });
763
764 if position.y > max_height {
765 max_height = position.y;
766 }
767
768 if self.inputs.enable_pitch_damping {
770 let temp_c = self.atmosphere.temperature;
771 let temp_k = temp_c + 273.15;
772 let speed_of_sound = (1.4 * 287.05 * temp_k).sqrt();
773 let mach = velocity_magnitude / speed_of_sound;
774
775 if transonic_mach.is_none() && mach < 1.2 && mach > 0.8 {
777 transonic_mach = Some(mach);
778 }
779
780 let bullet_type = if let Some(ref model) = self.inputs.bullet_model {
782 model.as_str()
783 } else {
784 "default"
785 };
786 let coeffs = PitchDampingCoefficients::from_bullet_type(bullet_type);
787 let pitch_damping = calculate_pitch_damping_coefficient(mach, &coeffs);
788
789 if pitch_damping < min_pitch_damping {
791 min_pitch_damping = pitch_damping;
792 }
793 }
794
795 if self.inputs.enable_precession_nutation {
797 if let Some(ref mut state) = angular_state {
798 let velocity_magnitude = velocity.magnitude();
799 let temp_c = self.atmosphere.temperature;
800 let temp_k = temp_c + 273.15;
801 let speed_of_sound = (1.4 * 287.05 * temp_k).sqrt();
802 let mach = velocity_magnitude / speed_of_sound;
803
804 let spin_rate_rad_s = if self.inputs.twist_rate > 0.0 {
806 let velocity_fps = velocity_magnitude * 3.28084;
807 let twist_rate_ft = self.inputs.twist_rate / 12.0;
808 (velocity_fps / twist_rate_ft) * 2.0 * std::f64::consts::PI
809 } else {
810 0.0
811 };
812
813 let params = PrecessionNutationParams {
815 mass_kg: self.inputs.bullet_mass,
816 caliber_m: self.inputs.bullet_diameter,
817 length_m: self.inputs.bullet_length,
818 spin_rate_rad_s,
819 spin_inertia: 6.94e-8, transverse_inertia: 9.13e-7, velocity_mps: velocity_magnitude,
822 air_density_kg_m3: air_density,
823 mach,
824 pitch_damping_coeff: -0.8,
825 nutation_damping_factor: 0.05,
826 };
827
828 *state = calculate_combined_angular_motion(
830 ¶ms,
831 state,
832 time,
833 self.time_step,
834 0.001, );
836
837 if state.yaw_angle.abs() > max_yaw_angle {
839 max_yaw_angle = state.yaw_angle.abs();
840 }
841 if state.precession_angle.abs() > max_precession_angle {
842 max_precession_angle = state.precession_angle.abs();
843 }
844 }
845 }
846
847 let dt = self.time_step;
849
850 let acc1 = self.calculate_acceleration(&position, &velocity, air_density, &wind_vector);
852
853 let pos2 = position + velocity * (dt * 0.5);
855 let vel2 = velocity + acc1 * (dt * 0.5);
856 let acc2 = self.calculate_acceleration(&pos2, &vel2, air_density, &wind_vector);
857
858 let pos3 = position + vel2 * (dt * 0.5);
860 let vel3 = velocity + acc2 * (dt * 0.5);
861 let acc3 = self.calculate_acceleration(&pos3, &vel3, air_density, &wind_vector);
862
863 let pos4 = position + vel3 * dt;
865 let vel4 = velocity + acc3 * dt;
866 let acc4 = self.calculate_acceleration(&pos4, &vel4, air_density, &wind_vector);
867
868 position += (velocity + vel2 * 2.0 + vel3 * 2.0 + vel4) * (dt / 6.0);
870 velocity += (acc1 + acc2 * 2.0 + acc3 * 2.0 + acc4) * (dt / 6.0);
871 time += dt;
872 }
873
874 let last_point = points.last().ok_or("No trajectory points generated")?;
876
877 let sampled_points = if self.inputs.enable_trajectory_sampling {
879 let trajectory_data = TrajectoryData {
880 times: points.iter().map(|p| p.time).collect(),
881 positions: points.iter().map(|p| p.position).collect(),
882 velocities: points
883 .iter()
884 .map(|p| {
885 Vector3::new(0.0, 0.0, p.velocity_magnitude)
887 })
888 .collect(),
889 transonic_distances: Vec::new(), };
891
892 let sight_position_m = self.inputs.muzzle_height + self.inputs.sight_height;
897 let outputs = TrajectoryOutputs {
898 target_distance_horiz_m: last_point.position.x, target_vertical_height_m: sight_position_m,
900 time_of_flight_s: last_point.time,
901 max_ord_dist_horiz_m: max_height,
902 sight_height_m: sight_position_m,
903 };
904
905 let samples = sample_trajectory(
907 &trajectory_data,
908 &outputs,
909 self.inputs.sample_interval,
910 self.inputs.bullet_mass,
911 );
912 Some(samples)
913 } else {
914 None
915 };
916
917 Ok(TrajectoryResult {
918 max_range: last_point.position.x, max_height,
920 time_of_flight: last_point.time,
921 impact_velocity: last_point.velocity_magnitude,
922 impact_energy: last_point.kinetic_energy,
923 points,
924 sampled_points,
925 min_pitch_damping: if self.inputs.enable_pitch_damping {
926 Some(min_pitch_damping)
927 } else {
928 None
929 },
930 transonic_mach,
931 angular_state,
932 max_yaw_angle: if self.inputs.enable_precession_nutation {
933 Some(max_yaw_angle)
934 } else {
935 None
936 },
937 max_precession_angle: if self.inputs.enable_precession_nutation {
938 Some(max_precession_angle)
939 } else {
940 None
941 },
942 })
943 }
944
945 fn solve_rk45(&self) -> Result<TrajectoryResult, BallisticsError> {
946 let mut time = 0.0;
948 let mut position = Vector3::new(
951 0.0,
952 self.inputs.muzzle_height, 0.0,
954 );
955
956 let horizontal_velocity = self.inputs.muzzle_velocity * self.inputs.muzzle_angle.cos();
959 let mut velocity = Vector3::new(
960 horizontal_velocity * self.inputs.azimuth_angle.cos(), self.inputs.muzzle_velocity * self.inputs.muzzle_angle.sin(), horizontal_velocity * self.inputs.azimuth_angle.sin(), );
964
965 let mut points = Vec::new();
966 let mut max_height = position.y;
967 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;
975 const MAX_ITERATIONS: usize = 100000;
976
977 while position.x < self.max_range
978 && position.y > self.inputs.ground_threshold
979 && time < 100.0
980 {
981 iteration_count += 1;
983 if iteration_count > MAX_ITERATIONS {
984 break; }
986
987 let velocity_magnitude = velocity.magnitude();
989 let kinetic_energy = 0.5 * self.inputs.bullet_mass * velocity_magnitude.powi(2);
990
991 points.push(TrajectoryPoint {
992 time,
993 position: position,
994 velocity_magnitude,
995 kinetic_energy,
996 });
997
998 if position.y > max_height {
999 max_height = position.y;
1000 }
1001
1002 let air_density = calculate_air_density(&self.atmosphere);
1004 let wind_vector = Vector3::new(
1005 self.wind.speed * self.wind.direction.cos(), 0.0,
1007 self.wind.speed * self.wind.direction.sin(), );
1009
1010 let (new_pos, new_vel, new_dt) = self.rk45_step(
1012 &position,
1013 &velocity,
1014 dt,
1015 air_density,
1016 &wind_vector,
1017 tolerance,
1018 );
1019
1020 dt = (safety_factor * new_dt).clamp(min_dt, max_dt);
1022
1023 position = new_pos;
1025 velocity = new_vel;
1026 time += dt;
1027 }
1028
1029 if points.is_empty() {
1031 return Err(BallisticsError::from("No trajectory points calculated"));
1032 }
1033
1034 let last_point = points.last().unwrap();
1035
1036 let sampled_points = if self.inputs.enable_trajectory_sampling {
1038 let trajectory_data = TrajectoryData {
1040 times: points.iter().map(|p| p.time).collect(),
1041 positions: points.iter().map(|p| p.position).collect(),
1042 velocities: points
1043 .iter()
1044 .map(|p| {
1045 Vector3::new(0.0, 0.0, p.velocity_magnitude)
1047 })
1048 .collect(),
1049 transonic_distances: Vec::new(),
1050 };
1051
1052 let sight_position_m = self.inputs.muzzle_height + self.inputs.sight_height;
1057 let outputs = TrajectoryOutputs {
1058 target_distance_horiz_m: last_point.position.x,
1059 target_vertical_height_m: sight_position_m,
1060 time_of_flight_s: last_point.time,
1061 max_ord_dist_horiz_m: max_height,
1062 sight_height_m: sight_position_m,
1063 };
1064
1065 let samples = sample_trajectory(
1066 &trajectory_data,
1067 &outputs,
1068 self.inputs.sample_interval,
1069 self.inputs.bullet_mass,
1070 );
1071 Some(samples)
1072 } else {
1073 None
1074 };
1075
1076 Ok(TrajectoryResult {
1077 max_range: last_point.position.x, max_height,
1079 time_of_flight: last_point.time,
1080 impact_velocity: last_point.velocity_magnitude,
1081 impact_energy: last_point.kinetic_energy,
1082 points,
1083 sampled_points,
1084 min_pitch_damping: None,
1085 transonic_mach: None,
1086 angular_state: None,
1087 max_yaw_angle: None,
1088 max_precession_angle: None,
1089 })
1090 }
1091
1092 fn rk45_step(
1093 &self,
1094 position: &Vector3<f64>,
1095 velocity: &Vector3<f64>,
1096 dt: f64,
1097 air_density: f64,
1098 wind_vector: &Vector3<f64>,
1099 tolerance: f64,
1100 ) -> (Vector3<f64>, Vector3<f64>, f64) {
1101 const A21: f64 = 1.0 / 5.0;
1103 const A31: f64 = 3.0 / 40.0;
1104 const A32: f64 = 9.0 / 40.0;
1105 const A41: f64 = 44.0 / 45.0;
1106 const A42: f64 = -56.0 / 15.0;
1107 const A43: f64 = 32.0 / 9.0;
1108 const A51: f64 = 19372.0 / 6561.0;
1109 const A52: f64 = -25360.0 / 2187.0;
1110 const A53: f64 = 64448.0 / 6561.0;
1111 const A54: f64 = -212.0 / 729.0;
1112 const A61: f64 = 9017.0 / 3168.0;
1113 const A62: f64 = -355.0 / 33.0;
1114 const A63: f64 = 46732.0 / 5247.0;
1115 const A64: f64 = 49.0 / 176.0;
1116 const A65: f64 = -5103.0 / 18656.0;
1117 const A71: f64 = 35.0 / 384.0;
1118 const A73: f64 = 500.0 / 1113.0;
1119 const A74: f64 = 125.0 / 192.0;
1120 const A75: f64 = -2187.0 / 6784.0;
1121 const A76: f64 = 11.0 / 84.0;
1122
1123 const B1: f64 = 35.0 / 384.0;
1125 const B3: f64 = 500.0 / 1113.0;
1126 const B4: f64 = 125.0 / 192.0;
1127 const B5: f64 = -2187.0 / 6784.0;
1128 const B6: f64 = 11.0 / 84.0;
1129
1130 const B1_ERR: f64 = 5179.0 / 57600.0;
1132 const B3_ERR: f64 = 7571.0 / 16695.0;
1133 const B4_ERR: f64 = 393.0 / 640.0;
1134 const B5_ERR: f64 = -92097.0 / 339200.0;
1135 const B6_ERR: f64 = 187.0 / 2100.0;
1136 const B7_ERR: f64 = 1.0 / 40.0;
1137
1138 let k1_v = self.calculate_acceleration(position, velocity, air_density, wind_vector);
1140 let k1_p = *velocity;
1141
1142 let p2 = position + dt * A21 * k1_p;
1143 let v2 = velocity + dt * A21 * k1_v;
1144 let k2_v = self.calculate_acceleration(&p2, &v2, air_density, wind_vector);
1145 let k2_p = v2;
1146
1147 let p3 = position + dt * (A31 * k1_p + A32 * k2_p);
1148 let v3 = velocity + dt * (A31 * k1_v + A32 * k2_v);
1149 let k3_v = self.calculate_acceleration(&p3, &v3, air_density, wind_vector);
1150 let k3_p = v3;
1151
1152 let p4 = position + dt * (A41 * k1_p + A42 * k2_p + A43 * k3_p);
1153 let v4 = velocity + dt * (A41 * k1_v + A42 * k2_v + A43 * k3_v);
1154 let k4_v = self.calculate_acceleration(&p4, &v4, air_density, wind_vector);
1155 let k4_p = v4;
1156
1157 let p5 = position + dt * (A51 * k1_p + A52 * k2_p + A53 * k3_p + A54 * k4_p);
1158 let v5 = velocity + dt * (A51 * k1_v + A52 * k2_v + A53 * k3_v + A54 * k4_v);
1159 let k5_v = self.calculate_acceleration(&p5, &v5, air_density, wind_vector);
1160 let k5_p = v5;
1161
1162 let p6 = position + dt * (A61 * k1_p + A62 * k2_p + A63 * k3_p + A64 * k4_p + A65 * k5_p);
1163 let v6 = velocity + dt * (A61 * k1_v + A62 * k2_v + A63 * k3_v + A64 * k4_v + A65 * k5_v);
1164 let k6_v = self.calculate_acceleration(&p6, &v6, air_density, wind_vector);
1165 let k6_p = v6;
1166
1167 let p7 = position + dt * (A71 * k1_p + A73 * k3_p + A74 * k4_p + A75 * k5_p + A76 * k6_p);
1168 let v7 = velocity + dt * (A71 * k1_v + A73 * k3_v + A74 * k4_v + A75 * k5_v + A76 * k6_v);
1169 let k7_v = self.calculate_acceleration(&p7, &v7, air_density, wind_vector);
1170 let k7_p = v7;
1171
1172 let new_pos = position + dt * (B1 * k1_p + B3 * k3_p + B4 * k4_p + B5 * k5_p + B6 * k6_p);
1174 let new_vel = velocity + dt * (B1 * k1_v + B3 * k3_v + B4 * k4_v + B5 * k5_v + B6 * k6_v);
1175
1176 let pos_err = position
1178 + dt * (B1_ERR * k1_p
1179 + B3_ERR * k3_p
1180 + B4_ERR * k4_p
1181 + B5_ERR * k5_p
1182 + B6_ERR * k6_p
1183 + B7_ERR * k7_p);
1184 let vel_err = velocity
1185 + dt * (B1_ERR * k1_v
1186 + B3_ERR * k3_v
1187 + B4_ERR * k4_v
1188 + B5_ERR * k5_v
1189 + B6_ERR * k6_v
1190 + B7_ERR * k7_v);
1191
1192 let pos_error = (new_pos - pos_err).magnitude();
1194 let vel_error = (new_vel - vel_err).magnitude();
1195 let error = (pos_error + vel_error) / (1.0 + position.magnitude() + velocity.magnitude());
1196
1197 let dt_new = if error < tolerance {
1199 dt * (tolerance / error).powf(0.2).min(2.0)
1200 } else {
1201 dt * (tolerance / error).powf(0.25).max(0.1)
1202 };
1203
1204 (new_pos, new_vel, dt_new)
1205 }
1206
1207 fn calculate_acceleration(
1208 &self,
1209 position: &Vector3<f64>,
1210 velocity: &Vector3<f64>,
1211 air_density: f64,
1212 wind_vector: &Vector3<f64>,
1213 ) -> Vector3<f64> {
1214 let actual_wind = if self.inputs.enable_wind_shear {
1216 self.get_wind_at_altitude(position.y)
1217 } else {
1218 *wind_vector
1219 };
1220
1221 let relative_velocity = velocity - actual_wind;
1222 let velocity_magnitude = relative_velocity.magnitude();
1223
1224 if velocity_magnitude < 0.001 {
1225 return Vector3::new(0.0, -9.81, 0.0);
1226 }
1227
1228 let cd = self.calculate_drag_coefficient(velocity_magnitude);
1230
1231 let velocity_fps = velocity_magnitude * 3.28084;
1233
1234 let base_bc = if let Some(ref segments) = self.inputs.bc_segments_data {
1236 segments
1238 .iter()
1239 .find(|seg| velocity_fps >= seg.velocity_min && velocity_fps < seg.velocity_max)
1240 .map(|seg| seg.bc_value)
1241 .unwrap_or(self.inputs.bc_value)
1242 } else {
1243 self.inputs.bc_value
1244 };
1245
1246 let effective_bc = if let Some(ref cluster_bc) = self.cluster_bc {
1248 cluster_bc.apply_correction(
1249 base_bc,
1250 self.inputs.caliber_inches, self.inputs.weight_grains,
1252 velocity_fps,
1253 )
1254 } else {
1255 base_bc
1256 };
1257
1258 let cd_to_retard = 0.000683 * 0.30; let standard_factor = cd * cd_to_retard;
1264 let density_scale = air_density / 1.225; let a_drag_ft_s2 =
1268 (velocity_fps * velocity_fps) * standard_factor * density_scale / effective_bc;
1269 let a_drag_m_s2 = a_drag_ft_s2 * 0.3048; let drag_acceleration = -a_drag_m_s2 * (relative_velocity / velocity_magnitude);
1273
1274 let mut accel = drag_acceleration + Vector3::new(0.0, -9.81, 0.0);
1276
1277 if self.inputs.enable_coriolis {
1280 if let Some(lat_deg) = self.inputs.latitude {
1281 let omega_earth = 7.2921159e-5_f64; let lat = lat_deg.to_radians();
1283 let az = self.inputs.azimuth_angle;
1284 let omega = Vector3::new(
1285 omega_earth * lat.cos() * az.cos(), omega_earth * lat.sin(), omega_earth * lat.cos() * az.sin(), );
1289 accel += 2.0 * omega.cross(velocity);
1295 }
1296 }
1297
1298 if self.inputs.enable_magnus
1300 && self.inputs.bullet_diameter > 0.0
1301 && self.inputs.twist_rate > 0.0
1302 {
1303 let (_, spin_rad_s) =
1304 crate::spin_drift::calculate_spin_rate(velocity_magnitude, self.inputs.twist_rate);
1305 let temp_k = self.atmosphere.temperature + 273.15;
1306 let speed_of_sound = (1.4 * 287.05 * temp_k).sqrt();
1307 let mach = velocity_magnitude / speed_of_sound;
1308
1309 let d_in = self.inputs.bullet_diameter / 0.0254;
1311 let m_gr = self.inputs.bullet_mass / 0.00006479891;
1312 let l_in = if self.inputs.bullet_length > 0.0 {
1313 self.inputs.bullet_length / 0.0254
1314 } else {
1315 4.5 * d_in
1316 };
1317 let sg = crate::spin_drift::miller_stability(d_in, m_gr, self.inputs.twist_rate, l_in);
1318
1319 let (yaw_rad, _) = crate::spin_drift::calculate_yaw_of_repose(
1321 sg,
1322 velocity_magnitude,
1323 spin_rad_s,
1324 0.0, 0.0, air_density,
1327 d_in,
1328 l_in,
1329 m_gr,
1330 mach,
1331 "match",
1332 false,
1333 );
1334
1335 let diameter_m = self.inputs.bullet_diameter; let spin_param = spin_rad_s * diameter_m / (2.0 * velocity_magnitude);
1338 let c_np = crate::derivatives::calculate_magnus_moment_coefficient(mach);
1339 let area = std::f64::consts::PI * (diameter_m / 2.0).powi(2);
1340 let magnus_force = 0.5
1341 * air_density
1342 * velocity_magnitude.powi(2)
1343 * area
1344 * c_np
1345 * spin_param
1346 * yaw_rad.sin();
1347
1348 let velocity_unit = relative_velocity / velocity_magnitude;
1351 let up = Vector3::new(0.0, 1.0, 0.0);
1352 let mut dir = velocity_unit.cross(&up);
1353 let dir_norm = dir.norm();
1354 if dir_norm > 1e-12 && magnus_force.abs() > 1e-12 {
1355 dir /= dir_norm;
1356 if !self.inputs.is_twist_right {
1357 dir = -dir;
1358 }
1359 accel += (magnus_force / self.inputs.bullet_mass) * dir;
1360 }
1361 }
1362
1363 accel
1364 }
1365
1366 fn calculate_drag_coefficient(&self, velocity: f64) -> f64 {
1367 let temp_c = self.atmosphere.temperature;
1369 let temp_k = temp_c + 273.15;
1370 let gamma = 1.4; let r_specific = 287.05; let speed_of_sound = (gamma * r_specific * temp_k).sqrt();
1373 let mach = velocity / speed_of_sound;
1374
1375 let base_cd = crate::drag::get_drag_coefficient(mach, &self.inputs.bc_type);
1377
1378 let projectile_shape = if let Some(ref model) = self.inputs.bullet_model {
1380 if model.to_lowercase().contains("boat") || model.to_lowercase().contains("bt") {
1382 ProjectileShape::BoatTail
1383 } else if model.to_lowercase().contains("round") || model.to_lowercase().contains("rn")
1384 {
1385 ProjectileShape::RoundNose
1386 } else if model.to_lowercase().contains("flat") || model.to_lowercase().contains("fb") {
1387 ProjectileShape::FlatBase
1388 } else {
1389 get_projectile_shape(
1391 self.inputs.bullet_diameter,
1392 self.inputs.bullet_mass / 0.00006479891, &self.inputs.bc_type.to_string(),
1394 )
1395 }
1396 } else {
1397 get_projectile_shape(
1399 self.inputs.bullet_diameter,
1400 self.inputs.bullet_mass / 0.00006479891, &self.inputs.bc_type.to_string(),
1402 )
1403 };
1404
1405 let include_wave_drag = false;
1411 transonic_correction(mach, base_cd, projectile_shape, include_wave_drag)
1412 }
1413}
1414
1415#[derive(Debug, Clone)]
1417pub struct MonteCarloParams {
1418 pub num_simulations: usize,
1419 pub velocity_std_dev: f64,
1420 pub angle_std_dev: f64,
1421 pub bc_std_dev: f64,
1422 pub wind_speed_std_dev: f64,
1423 pub target_distance: Option<f64>,
1424 pub base_wind_speed: f64,
1425 pub base_wind_direction: f64,
1426 pub azimuth_std_dev: f64, }
1428
1429impl Default for MonteCarloParams {
1430 fn default() -> Self {
1431 Self {
1432 num_simulations: 1000,
1433 velocity_std_dev: 1.0,
1434 angle_std_dev: 0.001,
1435 bc_std_dev: 0.01,
1436 wind_speed_std_dev: 1.0,
1437 target_distance: None,
1438 base_wind_speed: 0.0,
1439 base_wind_direction: 0.0,
1440 azimuth_std_dev: 0.001, }
1442 }
1443}
1444
1445#[derive(Debug, Clone)]
1447pub struct MonteCarloResults {
1448 pub ranges: Vec<f64>,
1449 pub impact_velocities: Vec<f64>,
1450 pub impact_positions: Vec<Vector3<f64>>,
1451}
1452
1453pub fn run_monte_carlo(
1455 base_inputs: BallisticInputs,
1456 params: MonteCarloParams,
1457) -> Result<MonteCarloResults, BallisticsError> {
1458 let base_wind = WindConditions {
1459 speed: params.base_wind_speed,
1460 direction: params.base_wind_direction,
1461 };
1462 run_monte_carlo_with_wind(base_inputs, base_wind, params)
1463}
1464
1465pub fn run_monte_carlo_with_wind(
1467 base_inputs: BallisticInputs,
1468 base_wind: WindConditions,
1469 params: MonteCarloParams,
1470) -> Result<MonteCarloResults, BallisticsError> {
1471 use rand::thread_rng;
1472 use rand_distr::{Distribution, Normal};
1473
1474 let mut rng = thread_rng();
1475 let mut ranges = Vec::new();
1476 let mut impact_velocities = Vec::new();
1477 let mut impact_positions = Vec::new();
1478
1479 let baseline_solver =
1481 TrajectorySolver::new(base_inputs.clone(), base_wind.clone(), Default::default());
1482 let baseline_result = baseline_solver.solve()?;
1483
1484 let target_distance = params.target_distance.unwrap_or(baseline_result.max_range);
1486
1487 let baseline_at_target = baseline_result
1489 .position_at_range(target_distance)
1490 .ok_or("Could not interpolate baseline at target distance")?;
1491
1492 let velocity_dist = Normal::new(base_inputs.muzzle_velocity, params.velocity_std_dev)
1494 .map_err(|e| format!("Invalid velocity distribution: {}", e))?;
1495 let angle_dist = Normal::new(base_inputs.muzzle_angle, params.angle_std_dev)
1496 .map_err(|e| format!("Invalid angle distribution: {}", e))?;
1497 let bc_dist = Normal::new(base_inputs.bc_value, params.bc_std_dev)
1498 .map_err(|e| format!("Invalid BC distribution: {}", e))?;
1499 let wind_speed_dist = Normal::new(base_wind.speed, params.wind_speed_std_dev)
1500 .map_err(|e| format!("Invalid wind speed distribution: {}", e))?;
1501 let wind_dir_dist =
1502 Normal::new(base_wind.direction, params.wind_speed_std_dev * 0.1) .map_err(|e| format!("Invalid wind direction distribution: {}", e))?;
1504 let azimuth_dist = Normal::new(base_inputs.azimuth_angle, params.azimuth_std_dev)
1505 .map_err(|e| format!("Invalid azimuth distribution: {}", e))?;
1506
1507 let pointing_error_dist = Normal::new(0.0, params.angle_std_dev * target_distance)
1509 .map_err(|e| format!("Invalid pointing distribution: {}", e))?;
1510
1511 for _ in 0..params.num_simulations {
1512 let mut inputs = base_inputs.clone();
1514 inputs.muzzle_velocity = velocity_dist.sample(&mut rng).max(0.0);
1515 inputs.muzzle_angle = angle_dist.sample(&mut rng);
1516 inputs.bc_value = bc_dist.sample(&mut rng).max(0.01);
1517 inputs.azimuth_angle = azimuth_dist.sample(&mut rng); let wind = WindConditions {
1521 speed: wind_speed_dist.sample(&mut rng).abs(),
1522 direction: wind_dir_dist.sample(&mut rng),
1523 };
1524
1525 let solver = TrajectorySolver::new(inputs, wind, Default::default());
1527 match solver.solve() {
1528 Ok(result) => {
1529 ranges.push(result.max_range);
1530 impact_velocities.push(result.impact_velocity);
1531
1532 if let Some(pos_at_target) = result.position_at_range(target_distance) {
1534 let mut deviation = Vector3::new(
1537 0.0, pos_at_target.y - baseline_at_target.y, pos_at_target.z - baseline_at_target.z, );
1541
1542 let pointing_error_y = pointing_error_dist.sample(&mut rng);
1545 deviation.y += pointing_error_y;
1546
1547 impact_positions.push(deviation);
1548 }
1549 }
1550 Err(_) => {
1551 continue;
1553 }
1554 }
1555 }
1556
1557 if ranges.is_empty() {
1558 return Err("No successful simulations".into());
1559 }
1560
1561 Ok(MonteCarloResults {
1562 ranges,
1563 impact_velocities,
1564 impact_positions,
1565 })
1566}
1567
1568pub fn calculate_zero_angle(
1570 inputs: BallisticInputs,
1571 target_distance: f64,
1572 target_height: f64,
1573) -> Result<f64, BallisticsError> {
1574 calculate_zero_angle_with_conditions(
1575 inputs,
1576 target_distance,
1577 target_height,
1578 WindConditions::default(),
1579 AtmosphericConditions::default(),
1580 )
1581}
1582
1583pub fn calculate_zero_angle_with_conditions(
1584 inputs: BallisticInputs,
1585 target_distance: f64,
1586 target_height: f64,
1587 wind: WindConditions,
1588 atmosphere: AtmosphericConditions,
1589) -> Result<f64, BallisticsError> {
1590 let get_height_at_angle = |angle: f64| -> Result<Option<f64>, BallisticsError> {
1592 let mut test_inputs = inputs.clone();
1593 test_inputs.muzzle_angle = angle;
1594
1595 let mut solver = TrajectorySolver::new(test_inputs, wind.clone(), atmosphere.clone());
1596 solver.set_max_range(target_distance * 2.0);
1597 solver.set_time_step(0.001);
1598 let result = solver.solve()?;
1599
1600 for i in 0..result.points.len() {
1602 if result.points[i].position.x >= target_distance {
1603 if i > 0 {
1604 let p1 = &result.points[i - 1];
1605 let p2 = &result.points[i];
1606 let t = (target_distance - p1.position.x) / (p2.position.x - p1.position.x);
1607 return Ok(Some(p1.position.y + t * (p2.position.y - p1.position.y)));
1608 } else {
1609 return Ok(Some(result.points[i].position.y));
1610 }
1611 }
1612 }
1613 Ok(None)
1614 };
1615
1616 let mut low_angle = 0.0; let mut high_angle = 0.2; let tolerance = 0.00001; let max_iterations = 50;
1622
1623 let low_height = get_height_at_angle(low_angle)?;
1626 let high_height = get_height_at_angle(high_angle)?;
1627
1628 match (low_height, high_height) {
1629 (Some(lh), Some(hh)) => {
1630 let low_error = lh - target_height;
1631 let high_error = hh - target_height;
1632
1633 if low_error > 0.0 && high_error > 0.0 {
1636 } else if low_error < 0.0 && high_error < 0.0 {
1641 let mut expanded = false;
1644 for multiplier in [2.0, 3.0, 4.0] {
1645 let new_high = (high_angle * multiplier).min(0.785);
1646 if let Ok(Some(h)) = get_height_at_angle(new_high) {
1647 if h - target_height > 0.0 {
1648 high_angle = new_high;
1649 expanded = true;
1650 break;
1651 }
1652 }
1653 if new_high >= 0.785 {
1654 break;
1655 }
1656 }
1657 if !expanded {
1658 return Err("Cannot find zero angle: target beyond effective range even at maximum angle".into());
1659 }
1660 }
1661 }
1663 (None, Some(_hh)) => {
1664 }
1667 (Some(_lh), None) => {
1668 return Err(
1670 "Cannot find zero angle: high angle trajectory doesn't reach target distance"
1671 .into(),
1672 );
1673 }
1674 (None, None) => {
1675 return Err(
1677 "Cannot find zero angle: trajectory cannot reach target distance at any angle"
1678 .into(),
1679 );
1680 }
1681 }
1682
1683 for _iteration in 0..max_iterations {
1684 let mid_angle = (low_angle + high_angle) / 2.0;
1685
1686 let mut test_inputs = inputs.clone();
1687 test_inputs.muzzle_angle = mid_angle;
1688
1689 let mut solver = TrajectorySolver::new(test_inputs, wind.clone(), atmosphere.clone());
1690 solver.set_max_range(target_distance * 2.0);
1692 solver.set_time_step(0.001);
1693 let result = solver.solve()?;
1694
1695 let mut height_at_target = None;
1697 for i in 0..result.points.len() {
1698 if result.points[i].position.x >= target_distance {
1699 if i > 0 {
1700 let p1 = &result.points[i - 1];
1702 let p2 = &result.points[i];
1703 let t = (target_distance - p1.position.x) / (p2.position.x - p1.position.x);
1704 height_at_target = Some(p1.position.y + t * (p2.position.y - p1.position.y));
1705 } else {
1706 height_at_target = Some(result.points[i].position.y);
1707 }
1708 break;
1709 }
1710 }
1711
1712 match height_at_target {
1713 Some(height) => {
1714 let error = height - target_height;
1715 if error.abs() < 0.001 {
1718 return Ok(mid_angle);
1719 }
1720
1721 if (high_angle - low_angle).abs() < tolerance {
1725 if error.abs() < 0.01 {
1726 return Ok(mid_angle);
1728 }
1729 return Ok(mid_angle);
1732 }
1733
1734 if error > 0.0 {
1735 high_angle = mid_angle;
1736 } else {
1737 low_angle = mid_angle;
1738 }
1739 }
1740 None => {
1741 low_angle = mid_angle;
1743
1744 if (high_angle - low_angle).abs() < tolerance {
1746 return Err("Trajectory cannot reach target distance - angle converged without valid solution".into());
1747 }
1748 }
1749 }
1750 }
1751
1752 Err("Failed to find zero angle".into())
1753}
1754
1755pub fn estimate_bc_from_trajectory(
1757 velocity: f64,
1758 mass: f64,
1759 diameter: f64,
1760 points: &[(f64, f64)], ) -> Result<f64, BallisticsError> {
1762 let mut best_bc = 0.5;
1764 let mut best_error = f64::MAX;
1765 let mut found_valid = false;
1766
1767 for bc in (100..1000).step_by(10) {
1769 let bc_value = bc as f64 / 1000.0;
1770
1771 let inputs = BallisticInputs {
1772 muzzle_velocity: velocity,
1773 bc_value,
1774 bullet_mass: mass,
1775 bullet_diameter: diameter,
1776 ..Default::default()
1777 };
1778
1779 let mut solver = TrajectorySolver::new(inputs, Default::default(), Default::default());
1780 solver.set_max_range(points.last().map(|(d, _)| *d * 1.5).unwrap_or(1000.0));
1782
1783 let result = match solver.solve() {
1784 Ok(r) => r,
1785 Err(_) => continue, };
1787
1788 let mut total_error = 0.0;
1790 for (target_dist, target_drop) in points {
1791 let mut calculated_drop = None;
1793 for i in 0..result.points.len() {
1794 if result.points[i].position.x >= *target_dist {
1795 if i > 0 {
1796 let p1 = &result.points[i - 1];
1798 let p2 = &result.points[i];
1799 let t = (target_dist - p1.position.x) / (p2.position.x - p1.position.x);
1800 calculated_drop =
1801 Some(-(p1.position.y + t * (p2.position.y - p1.position.y)));
1802 } else {
1803 calculated_drop = Some(-result.points[i].position.y);
1804 }
1805 break;
1806 }
1807 }
1808
1809 if let Some(drop) = calculated_drop {
1810 let error = (drop - target_drop).abs();
1811 total_error += error * error;
1812 }
1813 }
1814
1815 if total_error < best_error {
1816 best_error = total_error;
1817 best_bc = bc_value;
1818 found_valid = true;
1819 }
1820 }
1821
1822 if !found_valid {
1823 return Err(BallisticsError::from("Unable to estimate BC from provided data. Check that drop values are in correct units.".to_string()));
1824 }
1825
1826 Ok(best_bc)
1827}
1828
1829fn calculate_air_density(atmosphere: &AtmosphericConditions) -> f64 {
1831 let r_specific = 287.058; let temperature_k = atmosphere.temperature + 273.15;
1835
1836 let pressure_pa = atmosphere.pressure * 100.0;
1838
1839 let density = pressure_pa / (r_specific * temperature_k);
1841
1842 let altitude_factor = (-atmosphere.altitude / 8000.0).exp();
1844
1845 density * altitude_factor
1846}
1847
1848use rand;
1850use rand_distr;