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: 0.0, 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.cos(), self.inputs.muzzle_velocity * self.inputs.muzzle_angle.sin(), horizontal_velocity * self.inputs.azimuth_angle.sin(), );
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.cos(), 0.0,
396 self.wind.speed * self.wind.direction.sin(), );
398
399 while position.x < 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 points.len() == 1 || points.len() % 100 == 0 {
414 eprintln!("Trajectory point {}: time={:.3}s, x={:.2}m, y={:.2}m, z={:.2}m, vel={:.1}m/s",
415 points.len(), time, position.x, position.y, position.z, velocity_magnitude);
416 }
417
418 if position.y > max_height {
420 max_height = position.y;
421 }
422
423 if self.inputs.enable_pitch_damping {
425 let temp_c = self.atmosphere.temperature;
426 let temp_k = temp_c + 273.15;
427 let speed_of_sound = (1.4 * 287.05 * temp_k).sqrt();
428 let mach = velocity_magnitude / speed_of_sound;
429
430 if transonic_mach.is_none() && mach < 1.2 && mach > 0.8 {
432 transonic_mach = Some(mach);
433 }
434
435 let bullet_type = if let Some(ref model) = self.inputs.bullet_model {
437 model.as_str()
438 } else {
439 "default"
440 };
441 let coeffs = PitchDampingCoefficients::from_bullet_type(bullet_type);
442 let pitch_damping = calculate_pitch_damping_coefficient(mach, &coeffs);
443
444 if pitch_damping < min_pitch_damping {
446 min_pitch_damping = pitch_damping;
447 }
448 }
449
450 if self.inputs.enable_precession_nutation {
452 if let Some(ref mut state) = angular_state {
453 let velocity_magnitude = velocity.magnitude();
454 let temp_c = self.atmosphere.temperature;
455 let temp_k = temp_c + 273.15;
456 let speed_of_sound = (1.4 * 287.05 * temp_k).sqrt();
457 let mach = velocity_magnitude / speed_of_sound;
458
459 let spin_rate_rad_s = if self.inputs.twist_rate > 0.0 {
461 let velocity_fps = velocity_magnitude * 3.28084;
462 let twist_rate_ft = self.inputs.twist_rate / 12.0;
463 (velocity_fps / twist_rate_ft) * 2.0 * std::f64::consts::PI
464 } else {
465 0.0
466 };
467
468 let params = PrecessionNutationParams {
470 mass_kg: self.inputs.bullet_mass,
471 caliber_m: self.inputs.bullet_diameter,
472 length_m: self.inputs.bullet_length,
473 spin_rate_rad_s,
474 spin_inertia: 6.94e-8, transverse_inertia: 9.13e-7, velocity_mps: velocity_magnitude,
477 air_density_kg_m3: air_density,
478 mach,
479 pitch_damping_coeff: -0.8,
480 nutation_damping_factor: 0.05,
481 };
482
483 *state = calculate_combined_angular_motion(
485 ¶ms,
486 state,
487 time,
488 self.time_step,
489 0.001, );
491
492 if state.yaw_angle.abs() > max_yaw_angle {
494 max_yaw_angle = state.yaw_angle.abs();
495 }
496 if state.precession_angle.abs() > max_precession_angle {
497 max_precession_angle = state.precession_angle.abs();
498 }
499 }
500 }
501
502 let actual_wind = if self.inputs.enable_wind_shear {
504 self.get_wind_at_altitude(position.y)
505 } else {
506 wind_vector.clone()
507 };
508 let velocity_rel = velocity - actual_wind;
509 let velocity_rel_mag = velocity_rel.magnitude();
510 let drag_coefficient = self.calculate_drag_coefficient(velocity_rel_mag);
511
512 let drag_force = 0.5 * air_density * drag_coefficient *
514 self.inputs.bullet_diameter * self.inputs.bullet_diameter *
515 std::f64::consts::PI / 4.0 * velocity_rel_mag * velocity_rel_mag;
516
517 let drag_acceleration = -drag_force / self.inputs.bullet_mass;
519 let acceleration = Vector3::new(
520 drag_acceleration * velocity_rel.x / velocity_rel_mag,
521 drag_acceleration * velocity_rel.y / velocity_rel_mag - 9.80665,
522 drag_acceleration * velocity_rel.z / velocity_rel_mag,
523 );
524
525 velocity += acceleration * self.time_step;
527 position += velocity * self.time_step;
528 time += self.time_step;
529 }
530
531 let last_point = points.last().ok_or("No trajectory points generated")?;
533
534 let sampled_points = if self.inputs.enable_trajectory_sampling {
536 let trajectory_data = TrajectoryData {
537 times: points.iter().map(|p| p.time).collect(),
538 positions: points.iter().map(|p| p.position.clone()).collect(),
539 velocities: points.iter().map(|p| {
540 Vector3::new(0.0, 0.0, p.velocity_magnitude)
542 }).collect(),
543 transonic_distances: Vec::new(), };
545
546 let outputs = TrajectoryOutputs {
547 target_distance_horiz_m: last_point.position.x, target_vertical_height_m: last_point.position.y,
549 time_of_flight_s: last_point.time,
550 max_ord_dist_horiz_m: max_height,
551 };
552
553 let samples = sample_trajectory(&trajectory_data, &outputs, self.inputs.sample_interval, self.inputs.bullet_mass);
555 Some(samples)
556 } else {
557 None
558 };
559
560 Ok(TrajectoryResult {
561 max_range: last_point.position.x, max_height,
563 time_of_flight: last_point.time,
564 impact_velocity: last_point.velocity_magnitude,
565 impact_energy: last_point.kinetic_energy,
566 points,
567 sampled_points,
568 min_pitch_damping: if self.inputs.enable_pitch_damping { Some(min_pitch_damping) } else { None },
569 transonic_mach: transonic_mach,
570 angular_state: angular_state,
571 max_yaw_angle: if self.inputs.enable_precession_nutation { Some(max_yaw_angle) } else { None },
572 max_precession_angle: if self.inputs.enable_precession_nutation { Some(max_precession_angle) } else { None },
573 })
574 }
575
576 fn solve_rk4(&self) -> Result<TrajectoryResult, BallisticsError> {
577 let mut time = 0.0;
579 let mut position = Vector3::new(0.0, self.inputs.sight_height + self.inputs.muzzle_height, 0.0);
580
581 let horizontal_velocity = self.inputs.muzzle_velocity * self.inputs.muzzle_angle.cos();
583 let mut velocity = Vector3::new(
584 horizontal_velocity * self.inputs.azimuth_angle.cos(), self.inputs.muzzle_velocity * self.inputs.muzzle_angle.sin(), horizontal_velocity * self.inputs.azimuth_angle.sin(), );
588
589 let mut points = Vec::new();
590 let mut max_height = position.y;
591 let mut min_pitch_damping = 1.0; let mut transonic_mach = None; let mut angular_state = if self.inputs.enable_precession_nutation {
596 Some(AngularState {
597 pitch_angle: 0.001, yaw_angle: 0.001,
599 pitch_rate: 0.0,
600 yaw_rate: 0.0,
601 precession_angle: 0.0,
602 nutation_phase: 0.0,
603 })
604 } else {
605 None
606 };
607 let mut max_yaw_angle = 0.0;
608 let mut max_precession_angle = 0.0;
609
610 let air_density = calculate_air_density(&self.atmosphere);
612
613 let wind_vector = Vector3::new(
615 self.wind.speed * self.wind.direction.cos(), 0.0,
617 self.wind.speed * self.wind.direction.sin(), );
619
620 while position.x < self.max_range && position.y >= 0.0 && time < 100.0 {
622 let velocity_magnitude = velocity.magnitude();
624 let kinetic_energy = 0.5 * self.inputs.bullet_mass * velocity_magnitude * velocity_magnitude;
625
626 points.push(TrajectoryPoint {
627 time,
628 position: position.clone(),
629 velocity_magnitude,
630 kinetic_energy,
631 });
632
633 if position.y > max_height {
634 max_height = position.y;
635 }
636
637 if self.inputs.enable_pitch_damping {
639 let temp_c = self.atmosphere.temperature;
640 let temp_k = temp_c + 273.15;
641 let speed_of_sound = (1.4 * 287.05 * temp_k).sqrt();
642 let mach = velocity_magnitude / speed_of_sound;
643
644 if transonic_mach.is_none() && mach < 1.2 && mach > 0.8 {
646 transonic_mach = Some(mach);
647 }
648
649 let bullet_type = if let Some(ref model) = self.inputs.bullet_model {
651 model.as_str()
652 } else {
653 "default"
654 };
655 let coeffs = PitchDampingCoefficients::from_bullet_type(bullet_type);
656 let pitch_damping = calculate_pitch_damping_coefficient(mach, &coeffs);
657
658 if pitch_damping < min_pitch_damping {
660 min_pitch_damping = pitch_damping;
661 }
662 }
663
664 if self.inputs.enable_precession_nutation {
666 if let Some(ref mut state) = angular_state {
667 let velocity_magnitude = velocity.magnitude();
668 let temp_c = self.atmosphere.temperature;
669 let temp_k = temp_c + 273.15;
670 let speed_of_sound = (1.4 * 287.05 * temp_k).sqrt();
671 let mach = velocity_magnitude / speed_of_sound;
672
673 let spin_rate_rad_s = if self.inputs.twist_rate > 0.0 {
675 let velocity_fps = velocity_magnitude * 3.28084;
676 let twist_rate_ft = self.inputs.twist_rate / 12.0;
677 (velocity_fps / twist_rate_ft) * 2.0 * std::f64::consts::PI
678 } else {
679 0.0
680 };
681
682 let params = PrecessionNutationParams {
684 mass_kg: self.inputs.bullet_mass,
685 caliber_m: self.inputs.bullet_diameter,
686 length_m: self.inputs.bullet_length,
687 spin_rate_rad_s,
688 spin_inertia: 6.94e-8, transverse_inertia: 9.13e-7, velocity_mps: velocity_magnitude,
691 air_density_kg_m3: air_density,
692 mach,
693 pitch_damping_coeff: -0.8,
694 nutation_damping_factor: 0.05,
695 };
696
697 *state = calculate_combined_angular_motion(
699 ¶ms,
700 state,
701 time,
702 self.time_step,
703 0.001, );
705
706 if state.yaw_angle.abs() > max_yaw_angle {
708 max_yaw_angle = state.yaw_angle.abs();
709 }
710 if state.precession_angle.abs() > max_precession_angle {
711 max_precession_angle = state.precession_angle.abs();
712 }
713 }
714 }
715
716 let dt = self.time_step;
718
719 let acc1 = self.calculate_acceleration(&position, &velocity, air_density, &wind_vector);
721
722 let pos2 = position + velocity * (dt * 0.5);
724 let vel2 = velocity + acc1 * (dt * 0.5);
725 let acc2 = self.calculate_acceleration(&pos2, &vel2, air_density, &wind_vector);
726
727 let pos3 = position + vel2 * (dt * 0.5);
729 let vel3 = velocity + acc2 * (dt * 0.5);
730 let acc3 = self.calculate_acceleration(&pos3, &vel3, air_density, &wind_vector);
731
732 let pos4 = position + vel3 * dt;
734 let vel4 = velocity + acc3 * dt;
735 let acc4 = self.calculate_acceleration(&pos4, &vel4, air_density, &wind_vector);
736
737 position += (velocity + vel2 * 2.0 + vel3 * 2.0 + vel4) * (dt / 6.0);
739 velocity += (acc1 + acc2 * 2.0 + acc3 * 2.0 + acc4) * (dt / 6.0);
740 time += dt;
741 }
742
743 let last_point = points.last().ok_or("No trajectory points generated")?;
745
746 let sampled_points = if self.inputs.enable_trajectory_sampling {
748 let trajectory_data = TrajectoryData {
749 times: points.iter().map(|p| p.time).collect(),
750 positions: points.iter().map(|p| p.position.clone()).collect(),
751 velocities: points.iter().map(|p| {
752 Vector3::new(0.0, 0.0, p.velocity_magnitude)
754 }).collect(),
755 transonic_distances: Vec::new(), };
757
758 let outputs = TrajectoryOutputs {
759 target_distance_horiz_m: last_point.position.x, target_vertical_height_m: last_point.position.y,
761 time_of_flight_s: last_point.time,
762 max_ord_dist_horiz_m: max_height,
763 };
764
765 let samples = sample_trajectory(&trajectory_data, &outputs, self.inputs.sample_interval, self.inputs.bullet_mass);
767 Some(samples)
768 } else {
769 None
770 };
771
772 Ok(TrajectoryResult {
773 max_range: last_point.position.x, max_height,
775 time_of_flight: last_point.time,
776 impact_velocity: last_point.velocity_magnitude,
777 impact_energy: last_point.kinetic_energy,
778 points,
779 sampled_points,
780 min_pitch_damping: if self.inputs.enable_pitch_damping { Some(min_pitch_damping) } else { None },
781 transonic_mach: transonic_mach,
782 angular_state: angular_state,
783 max_yaw_angle: if self.inputs.enable_precession_nutation { Some(max_yaw_angle) } else { None },
784 max_precession_angle: if self.inputs.enable_precession_nutation { Some(max_precession_angle) } else { None },
785 })
786 }
787
788 fn solve_rk45(&self) -> Result<TrajectoryResult, BallisticsError> {
789 let mut time = 0.0;
791 let mut position = Vector3::new(0.0, self.inputs.sight_height + self.inputs.muzzle_height, 0.0);
792
793 let horizontal_velocity = self.inputs.muzzle_velocity * self.inputs.muzzle_angle.cos();
795 let mut velocity = Vector3::new(
796 horizontal_velocity * self.inputs.azimuth_angle.cos(), self.inputs.muzzle_velocity * self.inputs.muzzle_angle.sin(), horizontal_velocity * self.inputs.azimuth_angle.sin(), );
800
801 let mut points = Vec::new();
802 let mut max_height = position.y;
803 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;
811 const MAX_ITERATIONS: usize = 100000;
812
813 while position.x < self.max_range && position.y > self.inputs.ground_threshold && time < 100.0 { iteration_count += 1;
815 if iteration_count > MAX_ITERATIONS {
816 break; }
818
819 let velocity_magnitude = velocity.magnitude();
821 let kinetic_energy = 0.5 * self.inputs.bullet_mass * velocity_magnitude.powi(2);
822
823 points.push(TrajectoryPoint {
824 time,
825 position: position.clone(),
826 velocity_magnitude,
827 kinetic_energy,
828 });
829
830 if position.y > max_height {
831 max_height = position.y;
832 }
833
834 let air_density = calculate_air_density(&self.atmosphere);
836 let wind_vector = Vector3::new(
837 self.wind.speed * self.wind.direction.cos(), 0.0,
839 self.wind.speed * self.wind.direction.sin(), );
841
842 let (new_pos, new_vel, new_dt) = self.rk45_step(
844 &position,
845 &velocity,
846 dt,
847 air_density,
848 &wind_vector,
849 tolerance,
850 );
851
852 dt = (safety_factor * new_dt).clamp(min_dt, max_dt);
854
855 position = new_pos;
857 velocity = new_vel;
858 time += dt;
859 }
860
861 if points.is_empty() {
863 return Err(BallisticsError::from("No trajectory points calculated"));
864 }
865
866 let last_point = points.last().unwrap();
867
868 Ok(TrajectoryResult {
869 max_range: last_point.position.x, max_height,
871 time_of_flight: last_point.time,
872 impact_velocity: last_point.velocity_magnitude,
873 impact_energy: last_point.kinetic_energy,
874 points,
875 sampled_points: None, min_pitch_damping: None,
877 transonic_mach: None,
878 angular_state: None,
879 max_yaw_angle: None,
880 max_precession_angle: None,
881 })
882 }
883
884 fn rk45_step(
885 &self,
886 position: &Vector3<f64>,
887 velocity: &Vector3<f64>,
888 dt: f64,
889 air_density: f64,
890 wind_vector: &Vector3<f64>,
891 tolerance: f64,
892 ) -> (Vector3<f64>, Vector3<f64>, f64) {
893 const A21: f64 = 1.0 / 5.0;
895 const A31: f64 = 3.0 / 40.0;
896 const A32: f64 = 9.0 / 40.0;
897 const A41: f64 = 44.0 / 45.0;
898 const A42: f64 = -56.0 / 15.0;
899 const A43: f64 = 32.0 / 9.0;
900 const A51: f64 = 19372.0 / 6561.0;
901 const A52: f64 = -25360.0 / 2187.0;
902 const A53: f64 = 64448.0 / 6561.0;
903 const A54: f64 = -212.0 / 729.0;
904 const A61: f64 = 9017.0 / 3168.0;
905 const A62: f64 = -355.0 / 33.0;
906 const A63: f64 = 46732.0 / 5247.0;
907 const A64: f64 = 49.0 / 176.0;
908 const A65: f64 = -5103.0 / 18656.0;
909 const A71: f64 = 35.0 / 384.0;
910 const A73: f64 = 500.0 / 1113.0;
911 const A74: f64 = 125.0 / 192.0;
912 const A75: f64 = -2187.0 / 6784.0;
913 const A76: f64 = 11.0 / 84.0;
914
915 const B1: f64 = 35.0 / 384.0;
917 const B3: f64 = 500.0 / 1113.0;
918 const B4: f64 = 125.0 / 192.0;
919 const B5: f64 = -2187.0 / 6784.0;
920 const B6: f64 = 11.0 / 84.0;
921
922 const B1_ERR: f64 = 5179.0 / 57600.0;
924 const B3_ERR: f64 = 7571.0 / 16695.0;
925 const B4_ERR: f64 = 393.0 / 640.0;
926 const B5_ERR: f64 = -92097.0 / 339200.0;
927 const B6_ERR: f64 = 187.0 / 2100.0;
928 const B7_ERR: f64 = 1.0 / 40.0;
929
930 let k1_v = self.calculate_acceleration(position, velocity, air_density, wind_vector);
932 let k1_p = velocity.clone();
933
934 let p2 = position + dt * A21 * k1_p;
935 let v2 = velocity + dt * A21 * k1_v;
936 let k2_v = self.calculate_acceleration(&p2, &v2, air_density, wind_vector);
937 let k2_p = v2;
938
939 let p3 = position + dt * (A31 * k1_p + A32 * k2_p);
940 let v3 = velocity + dt * (A31 * k1_v + A32 * k2_v);
941 let k3_v = self.calculate_acceleration(&p3, &v3, air_density, wind_vector);
942 let k3_p = v3;
943
944 let p4 = position + dt * (A41 * k1_p + A42 * k2_p + A43 * k3_p);
945 let v4 = velocity + dt * (A41 * k1_v + A42 * k2_v + A43 * k3_v);
946 let k4_v = self.calculate_acceleration(&p4, &v4, air_density, wind_vector);
947 let k4_p = v4;
948
949 let p5 = position + dt * (A51 * k1_p + A52 * k2_p + A53 * k3_p + A54 * k4_p);
950 let v5 = velocity + dt * (A51 * k1_v + A52 * k2_v + A53 * k3_v + A54 * k4_v);
951 let k5_v = self.calculate_acceleration(&p5, &v5, air_density, wind_vector);
952 let k5_p = v5;
953
954 let p6 = position + dt * (A61 * k1_p + A62 * k2_p + A63 * k3_p + A64 * k4_p + A65 * k5_p);
955 let v6 = velocity + dt * (A61 * k1_v + A62 * k2_v + A63 * k3_v + A64 * k4_v + A65 * k5_v);
956 let k6_v = self.calculate_acceleration(&p6, &v6, air_density, wind_vector);
957 let k6_p = v6;
958
959 let p7 = position + dt * (A71 * k1_p + A73 * k3_p + A74 * k4_p + A75 * k5_p + A76 * k6_p);
960 let v7 = velocity + dt * (A71 * k1_v + A73 * k3_v + A74 * k4_v + A75 * k5_v + A76 * k6_v);
961 let k7_v = self.calculate_acceleration(&p7, &v7, air_density, wind_vector);
962 let k7_p = v7;
963
964 let new_pos = position + dt * (B1 * k1_p + B3 * k3_p + B4 * k4_p + B5 * k5_p + B6 * k6_p);
966 let new_vel = velocity + dt * (B1 * k1_v + B3 * k3_v + B4 * k4_v + B5 * k5_v + B6 * k6_v);
967
968 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);
970 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);
971
972 let pos_error = (new_pos - pos_err).magnitude();
974 let vel_error = (new_vel - vel_err).magnitude();
975 let error = (pos_error + vel_error) / (1.0 + position.magnitude() + velocity.magnitude());
976
977 let dt_new = if error < tolerance {
979 dt * (tolerance / error).powf(0.2).min(2.0)
980 } else {
981 dt * (tolerance / error).powf(0.25).max(0.1)
982 };
983
984 (new_pos, new_vel, dt_new)
985 }
986
987 fn calculate_acceleration(&self, position: &Vector3<f64>, velocity: &Vector3<f64>, air_density: f64, wind_vector: &Vector3<f64>) -> Vector3<f64> {
988 let actual_wind = if self.inputs.enable_wind_shear {
990 self.get_wind_at_altitude(position.y)
991 } else {
992 wind_vector.clone()
993 };
994
995 let relative_velocity = velocity - &actual_wind;
996 let velocity_magnitude = relative_velocity.magnitude();
997
998 if velocity_magnitude < 0.001 {
999 return Vector3::new(0.0, -9.81, 0.0);
1000 }
1001
1002 let cd = self.calculate_drag_coefficient(velocity_magnitude);
1004 let reference_area = std::f64::consts::PI * (self.inputs.bullet_diameter / 2.0).powi(2);
1005
1006 let effective_bc = if let Some(ref cluster_bc) = self.cluster_bc {
1008 let velocity_fps = velocity_magnitude * 3.28084;
1010 cluster_bc.apply_correction(
1011 self.inputs.bc_value,
1012 self.inputs.caliber_inches * 0.0254, self.inputs.weight_grains,
1014 velocity_fps
1015 )
1016 } else {
1017 self.inputs.bc_value
1018 };
1019
1020 let drag_magnitude = 0.5 * air_density * velocity_magnitude.powi(2) * cd * reference_area / effective_bc;
1021
1022 let drag_force = -relative_velocity.normalize() * drag_magnitude;
1024
1025 let acceleration = drag_force / self.inputs.bullet_mass + Vector3::new(0.0, -9.81, 0.0);
1027
1028 acceleration
1029 }
1030
1031 fn calculate_drag_coefficient(&self, velocity: f64) -> f64 {
1032 let temp_c = self.atmosphere.temperature;
1035 let temp_k = temp_c + 273.15;
1036 let gamma = 1.4; let r_specific = 287.05; let speed_of_sound = (gamma * r_specific * temp_k).sqrt();
1039 let mach = velocity / speed_of_sound;
1040
1041 let base_cd = match self.inputs.bc_type {
1043 DragModel::G1 => 0.5,
1044 DragModel::G7 => 0.4,
1045 _ => 0.45,
1046 };
1047
1048 let projectile_shape = if let Some(ref model) = self.inputs.bullet_model {
1050 if model.to_lowercase().contains("boat") || model.to_lowercase().contains("bt") {
1052 ProjectileShape::BoatTail
1053 } else if model.to_lowercase().contains("round") || model.to_lowercase().contains("rn") {
1054 ProjectileShape::RoundNose
1055 } else if model.to_lowercase().contains("flat") || model.to_lowercase().contains("fb") {
1056 ProjectileShape::FlatBase
1057 } else {
1058 get_projectile_shape(
1060 self.inputs.bullet_diameter,
1061 self.inputs.bullet_mass / 0.00006479891, &self.inputs.bc_type.to_string()
1063 )
1064 }
1065 } else {
1066 get_projectile_shape(
1068 self.inputs.bullet_diameter,
1069 self.inputs.bullet_mass / 0.00006479891, &self.inputs.bc_type.to_string()
1071 )
1072 };
1073
1074 let include_wave_drag = self.inputs.enable_advanced_effects;
1077 transonic_correction(mach, base_cd, projectile_shape, include_wave_drag)
1078 }
1079}
1080
1081#[derive(Debug, Clone)]
1083pub struct MonteCarloParams {
1084 pub num_simulations: usize,
1085 pub velocity_std_dev: f64,
1086 pub angle_std_dev: f64,
1087 pub bc_std_dev: f64,
1088 pub wind_speed_std_dev: f64,
1089 pub target_distance: Option<f64>,
1090 pub base_wind_speed: f64,
1091 pub base_wind_direction: f64,
1092 pub azimuth_std_dev: f64, }
1094
1095impl Default for MonteCarloParams {
1096 fn default() -> Self {
1097 Self {
1098 num_simulations: 1000,
1099 velocity_std_dev: 1.0,
1100 angle_std_dev: 0.001,
1101 bc_std_dev: 0.01,
1102 wind_speed_std_dev: 1.0,
1103 target_distance: None,
1104 base_wind_speed: 0.0,
1105 base_wind_direction: 0.0,
1106 azimuth_std_dev: 0.001, }
1108 }
1109}
1110
1111#[derive(Debug, Clone)]
1113pub struct MonteCarloResults {
1114 pub ranges: Vec<f64>,
1115 pub impact_velocities: Vec<f64>,
1116 pub impact_positions: Vec<Vector3<f64>>,
1117}
1118
1119pub fn run_monte_carlo(
1121 base_inputs: BallisticInputs,
1122 params: MonteCarloParams,
1123) -> Result<MonteCarloResults, BallisticsError> {
1124 let base_wind = WindConditions {
1125 speed: params.base_wind_speed,
1126 direction: params.base_wind_direction,
1127 };
1128 run_monte_carlo_with_wind(base_inputs, base_wind, params)
1129}
1130
1131pub fn run_monte_carlo_with_wind(
1133 base_inputs: BallisticInputs,
1134 base_wind: WindConditions,
1135 params: MonteCarloParams,
1136) -> Result<MonteCarloResults, BallisticsError> {
1137 use rand::thread_rng;
1138 use rand_distr::{Distribution, Normal};
1139
1140 let mut rng = thread_rng();
1141 let mut ranges = Vec::new();
1142 let mut impact_velocities = Vec::new();
1143 let mut impact_positions = Vec::new();
1144
1145 let baseline_solver = TrajectorySolver::new(base_inputs.clone(), base_wind.clone(), Default::default());
1147 let baseline_result = baseline_solver.solve()?;
1148 let baseline_impact = baseline_result.points.last()
1149 .ok_or("No baseline trajectory points")?
1150 .position.clone();
1151
1152 let velocity_dist = Normal::new(base_inputs.muzzle_velocity, params.velocity_std_dev)
1154 .map_err(|e| format!("Invalid velocity distribution: {}", e))?;
1155 let angle_dist = Normal::new(base_inputs.muzzle_angle, params.angle_std_dev)
1156 .map_err(|e| format!("Invalid angle distribution: {}", e))?;
1157 let bc_dist = Normal::new(base_inputs.bc_value, params.bc_std_dev)
1158 .map_err(|e| format!("Invalid BC distribution: {}", e))?;
1159 let wind_speed_dist = Normal::new(base_wind.speed, params.wind_speed_std_dev)
1160 .map_err(|e| format!("Invalid wind speed distribution: {}", e))?;
1161 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))?;
1163 let azimuth_dist = Normal::new(base_inputs.azimuth_angle, params.azimuth_std_dev)
1164 .map_err(|e| format!("Invalid azimuth distribution: {}", e))?;
1165
1166 let distance = baseline_impact.z; let pointing_error_dist = Normal::new(0.0, params.angle_std_dev * distance)
1169 .map_err(|e| format!("Invalid pointing distribution: {}", e))?;
1170
1171 for _ in 0..params.num_simulations {
1172 let mut inputs = base_inputs.clone();
1174 inputs.muzzle_velocity = velocity_dist.sample(&mut rng).max(0.0);
1175 inputs.muzzle_angle = angle_dist.sample(&mut rng);
1176 inputs.bc_value = bc_dist.sample(&mut rng).max(0.01);
1177 inputs.azimuth_angle = azimuth_dist.sample(&mut rng); let wind = WindConditions {
1181 speed: wind_speed_dist.sample(&mut rng).abs(),
1182 direction: wind_dir_dist.sample(&mut rng),
1183 };
1184
1185 let solver = TrajectorySolver::new(inputs, wind, Default::default());
1187 match solver.solve() {
1188 Ok(result) => {
1189 ranges.push(result.max_range);
1190 impact_velocities.push(result.impact_velocity);
1191 if let Some(last_point) = result.points.last() {
1192 let mut deviation = Vector3::new(
1194 last_point.position.x - baseline_impact.x, last_point.position.y - baseline_impact.y, last_point.position.z - baseline_impact.z, );
1198
1199 let pointing_error_y = pointing_error_dist.sample(&mut rng);
1202 deviation.y += pointing_error_y;
1203
1204 impact_positions.push(deviation);
1205 }
1206 },
1207 Err(_) => {
1208 continue;
1210 }
1211 }
1212 }
1213
1214 if ranges.is_empty() {
1215 return Err("No successful simulations".into());
1216 }
1217
1218 Ok(MonteCarloResults {
1219 ranges,
1220 impact_velocities,
1221 impact_positions,
1222 })
1223}
1224
1225pub fn calculate_zero_angle(
1227 inputs: BallisticInputs,
1228 target_distance: f64,
1229 target_height: f64,
1230) -> Result<f64, BallisticsError> {
1231 calculate_zero_angle_with_conditions(
1232 inputs,
1233 target_distance,
1234 target_height,
1235 WindConditions::default(),
1236 AtmosphericConditions::default(),
1237 )
1238}
1239
1240pub fn calculate_zero_angle_with_conditions(
1241 inputs: BallisticInputs,
1242 target_distance: f64,
1243 target_height: f64,
1244 wind: WindConditions,
1245 atmosphere: AtmosphericConditions,
1246) -> Result<f64, BallisticsError> {
1247 let mut low_angle = 0.0; let mut high_angle = 0.2; let tolerance = 0.00001; let max_iterations = 50;
1253
1254 for iteration in 0..max_iterations {
1255 let mid_angle = (low_angle + high_angle) / 2.0;
1256
1257 let mut test_inputs = inputs.clone();
1258 test_inputs.muzzle_angle = mid_angle;
1259
1260 let mut solver = TrajectorySolver::new(test_inputs, wind.clone(), atmosphere.clone());
1261 solver.set_max_range(target_distance * 2.0);
1263 solver.set_time_step(0.001);
1264 let result = solver.solve()?;
1265
1266 let mut height_at_target = None;
1268 for i in 0..result.points.len() {
1269 if result.points[i].position.x >= target_distance {
1270 if i > 0 {
1271 let p1 = &result.points[i - 1];
1273 let p2 = &result.points[i];
1274 let t = (target_distance - p1.position.x) / (p2.position.x - p1.position.x);
1275 height_at_target = Some(p1.position.y + t * (p2.position.y - p1.position.y));
1276 } else {
1277 height_at_target = Some(result.points[i].position.y);
1278 }
1279 break;
1280 }
1281 }
1282
1283 match height_at_target {
1284 Some(height) => {
1285 let error = height - target_height;
1286 if error.abs() < 0.001 {
1287 return Ok(mid_angle);
1288 }
1289
1290 if error > 0.0 {
1291 high_angle = mid_angle;
1292 } else {
1293 low_angle = mid_angle;
1294 }
1295 },
1296 None => {
1297 low_angle = mid_angle;
1299 }
1300 }
1301
1302 if (high_angle - low_angle).abs() < tolerance {
1303 return Ok(mid_angle);
1304 }
1305 }
1306
1307 Err("Failed to find zero angle".into())
1308}
1309
1310pub fn estimate_bc_from_trajectory(
1312 velocity: f64,
1313 mass: f64,
1314 diameter: f64,
1315 points: &[(f64, f64)], ) -> Result<f64, BallisticsError> {
1317 let mut best_bc = 0.5;
1319 let mut best_error = f64::MAX;
1320 let mut found_valid = false;
1321
1322 for bc in (100..1000).step_by(10) {
1324 let bc_value = bc as f64 / 1000.0;
1325
1326 let inputs = BallisticInputs {
1327 muzzle_velocity: velocity,
1328 bc_value,
1329 bullet_mass: mass,
1330 bullet_diameter: diameter,
1331 ..Default::default()
1332 };
1333
1334 let mut solver = TrajectorySolver::new(inputs, Default::default(), Default::default());
1335 solver.set_max_range(points.last().map(|(d, _)| *d * 1.5).unwrap_or(1000.0));
1337
1338 let result = match solver.solve() {
1339 Ok(r) => r,
1340 Err(_) => continue, };
1342
1343 let mut total_error = 0.0;
1345 for (target_dist, target_drop) in points {
1346 let mut calculated_drop = None;
1348 for i in 0..result.points.len() {
1349 if result.points[i].position.z >= *target_dist {
1350 if i > 0 {
1351 let p1 = &result.points[i - 1];
1353 let p2 = &result.points[i];
1354 let t = (target_dist - p1.position.z) / (p2.position.z - p1.position.z);
1355 calculated_drop = Some(-(p1.position.y + t * (p2.position.y - p1.position.y)));
1356 } else {
1357 calculated_drop = Some(-result.points[i].position.y);
1358 }
1359 break;
1360 }
1361 }
1362
1363 if let Some(drop) = calculated_drop {
1364 let error = (drop - target_drop).abs();
1365 total_error += error * error;
1366 }
1367 }
1368
1369 if total_error < best_error {
1370 best_error = total_error;
1371 best_bc = bc_value;
1372 found_valid = true;
1373 }
1374 }
1375
1376 if !found_valid {
1377 return Err(BallisticsError::from("Unable to estimate BC from provided data. Check that drop values are in correct units.".to_string()));
1378 }
1379
1380 Ok(best_bc)
1381}
1382
1383fn calculate_air_density(atmosphere: &AtmosphericConditions) -> f64 {
1385 let r_specific = 287.058; let temperature_k = atmosphere.temperature + 273.15;
1389
1390 let pressure_pa = atmosphere.pressure * 100.0;
1392
1393 let density = pressure_pa / (r_specific * temperature_k);
1395
1396 let altitude_factor = (-atmosphere.altitude / 8000.0).exp();
1398
1399 density * altitude_factor
1400}
1401
1402use rand;
1404use rand_distr;