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 nalgebra::Vector3;
9use std::error::Error;
10use std::fmt;
11
12#[derive(Debug, Clone, Copy, PartialEq)]
14pub enum UnitSystem {
15 Imperial,
16 Metric,
17}
18
19#[derive(Debug, Clone, Copy, PartialEq)]
21pub enum OutputFormat {
22 Table,
23 Json,
24 Csv,
25}
26
27#[derive(Debug)]
29pub struct BallisticsError {
30 message: String,
31}
32
33impl fmt::Display for BallisticsError {
34 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
35 write!(f, "{}", self.message)
36 }
37}
38
39impl Error for BallisticsError {}
40
41impl From<String> for BallisticsError {
42 fn from(msg: String) -> Self {
43 BallisticsError { message: msg }
44 }
45}
46
47impl From<&str> for BallisticsError {
48 fn from(msg: &str) -> Self {
49 BallisticsError { message: msg.to_string() }
50 }
51}
52
53#[derive(Debug, Clone)]
55pub struct BallisticInputs {
56 pub muzzle_velocity: f64, pub launch_angle: f64, pub ballistic_coefficient: f64,
60 pub mass: f64, pub diameter: f64, pub drag_model: DragModel,
63 pub sight_height: f64, pub altitude: f64, pub bc_type: DragModel, pub bc_value: f64, pub caliber_inches: f64, pub weight_grains: f64, pub bullet_diameter: f64, pub bullet_mass: f64, pub bullet_length: f64, pub muzzle_angle: f64, pub target_distance: f64, pub azimuth_angle: f64, pub use_rk4: bool, pub temperature: f64, pub twist_rate: f64, pub is_twist_right: bool, pub shooting_angle: f64, pub latitude: Option<f64>, pub ground_threshold: f64, pub enable_advanced_effects: bool,
87 pub use_powder_sensitivity: bool,
88 pub powder_temp_sensitivity: f64,
89 pub powder_temp: f64, pub tipoff_yaw: f64, pub tipoff_decay_distance: f64, pub use_bc_segments: bool,
93 pub bc_segments: Option<Vec<(f64, f64)>>, pub bc_segments_data: Option<Vec<crate::BCSegmentData>>, pub use_enhanced_spin_drift: bool,
96 pub use_form_factor: bool,
97 pub enable_wind_shear: bool,
98 pub wind_shear_model: String,
99 pub enable_trajectory_sampling: bool,
100 pub sample_interval: f64, pub enable_pitch_damping: bool,
102 pub enable_precession_nutation: bool,
103
104 pub bc_type_str: Option<String>,
106 pub bullet_model: Option<String>,
107 pub bullet_id: Option<String>,
108}
109
110impl Default for BallisticInputs {
111 fn default() -> Self {
112 let mass_kg = 0.01;
113 let diameter_m = 0.00762;
114 let bc = 0.5;
115 let launch_angle_rad = 0.0;
116 let drag_model = DragModel::G1;
117
118 Self {
119 muzzle_velocity: 800.0,
121 launch_angle: launch_angle_rad,
122 ballistic_coefficient: bc,
123 mass: mass_kg,
124 diameter: diameter_m,
125 drag_model,
126 sight_height: 0.05,
127
128 altitude: 0.0,
130 bc_type: drag_model,
131 bc_value: bc,
132 caliber_inches: diameter_m / 0.0254, weight_grains: mass_kg / 0.00006479891, bullet_diameter: diameter_m,
135 bullet_mass: mass_kg,
136 bullet_length: diameter_m * 4.0, muzzle_angle: launch_angle_rad,
138 target_distance: 100.0,
139 azimuth_angle: 0.0,
140 use_rk4: true, temperature: 15.0,
142 twist_rate: 12.0, is_twist_right: true,
144 shooting_angle: 0.0,
145 latitude: None,
146 ground_threshold: -10.0,
147
148 enable_advanced_effects: false,
150 use_powder_sensitivity: false,
151 powder_temp_sensitivity: 0.0,
152 powder_temp: 15.0,
153 tipoff_yaw: 0.0,
154 tipoff_decay_distance: 50.0,
155 use_bc_segments: false,
156 bc_segments: None,
157 bc_segments_data: None,
158 use_enhanced_spin_drift: false,
159 use_form_factor: false,
160 enable_wind_shear: false,
161 wind_shear_model: "none".to_string(),
162 enable_trajectory_sampling: false,
163 sample_interval: 10.0, enable_pitch_damping: false,
165 enable_precession_nutation: false,
166
167 bc_type_str: None,
169 bullet_model: None,
170 bullet_id: None,
171 }
172 }
173}
174
175#[derive(Debug, Clone)]
177pub struct WindConditions {
178 pub speed: f64, pub direction: f64, }
181
182impl Default for WindConditions {
183 fn default() -> Self {
184 Self {
185 speed: 0.0,
186 direction: 0.0,
187 }
188 }
189}
190
191#[derive(Debug, Clone)]
193pub struct AtmosphericConditions {
194 pub temperature: f64, pub pressure: f64, pub humidity: f64, pub altitude: f64, }
199
200impl Default for AtmosphericConditions {
201 fn default() -> Self {
202 Self {
203 temperature: 15.0,
204 pressure: 1013.25,
205 humidity: 50.0,
206 altitude: 0.0,
207 }
208 }
209}
210
211#[derive(Debug, Clone)]
213pub struct TrajectoryPoint {
214 pub time: f64,
215 pub position: Vector3<f64>,
216 pub velocity_magnitude: f64,
217 pub kinetic_energy: f64,
218}
219
220#[derive(Debug, Clone)]
222pub struct TrajectoryResult {
223 pub max_range: f64,
224 pub max_height: f64,
225 pub time_of_flight: f64,
226 pub impact_velocity: f64,
227 pub impact_energy: f64,
228 pub points: Vec<TrajectoryPoint>,
229 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>, }
236
237pub struct TrajectorySolver {
239 inputs: BallisticInputs,
240 wind: WindConditions,
241 atmosphere: AtmosphericConditions,
242 max_range: f64,
243 time_step: f64,
244}
245
246impl TrajectorySolver {
247 pub fn new(mut inputs: BallisticInputs, wind: WindConditions, atmosphere: AtmosphericConditions) -> Self {
248 inputs.bc_type = inputs.drag_model;
250 inputs.bc_value = inputs.ballistic_coefficient;
251 inputs.bullet_diameter = inputs.diameter;
252 inputs.bullet_mass = inputs.mass;
253 inputs.muzzle_angle = inputs.launch_angle;
254 inputs.caliber_inches = inputs.diameter / 0.0254;
255 inputs.weight_grains = inputs.mass / 0.00006479891;
256
257 Self {
258 inputs,
259 wind,
260 atmosphere,
261 max_range: 1000.0,
262 time_step: 0.001,
263 }
264 }
265
266 pub fn set_max_range(&mut self, range: f64) {
267 self.max_range = range;
268 }
269
270 pub fn set_time_step(&mut self, step: f64) {
271 self.time_step = step;
272 }
273
274 fn get_wind_at_altitude(&self, altitude_m: f64) -> Vector3<f64> {
275 let profile = WindShearProfile {
277 model: if self.inputs.wind_shear_model == "logarithmic" {
278 WindShearModel::Logarithmic
279 } else if self.inputs.wind_shear_model == "power" {
280 WindShearModel::PowerLaw
281 } else {
282 WindShearModel::PowerLaw },
284 surface_wind: WindLayer {
285 altitude_m: 0.0,
286 speed_mps: self.wind.speed,
287 direction_deg: self.wind.direction.to_degrees(),
288 },
289 reference_height: 10.0, roughness_length: 0.03, power_exponent: 1.0 / 7.0, geostrophic_wind: None,
293 custom_layers: Vec::new(),
294 };
295
296 profile.get_wind_at_altitude(altitude_m)
297 }
298
299 pub fn solve(&self) -> Result<TrajectoryResult, BallisticsError> {
300 if self.inputs.use_rk4 {
301 self.solve_rk4()
302 } else {
303 self.solve_euler()
304 }
305 }
306
307 fn solve_euler(&self) -> Result<TrajectoryResult, BallisticsError> {
308 let mut time = 0.0;
310 let mut position = Vector3::new(0.0, self.inputs.sight_height, 0.0);
311 let horizontal_velocity = self.inputs.muzzle_velocity * self.inputs.launch_angle.cos();
313 let mut velocity = Vector3::new(
314 horizontal_velocity * self.inputs.azimuth_angle.sin(), self.inputs.muzzle_velocity * self.inputs.launch_angle.sin(), horizontal_velocity * self.inputs.azimuth_angle.cos(), );
318
319 let mut points = Vec::new();
320 let mut max_height = position.y;
321 let mut min_pitch_damping = 1.0; let mut transonic_mach = None; let mut angular_state = if self.inputs.enable_precession_nutation {
326 Some(AngularState {
327 pitch_angle: 0.001, yaw_angle: 0.001,
329 pitch_rate: 0.0,
330 yaw_rate: 0.0,
331 precession_angle: 0.0,
332 nutation_phase: 0.0,
333 })
334 } else {
335 None
336 };
337 let mut max_yaw_angle = 0.0;
338 let mut max_precession_angle = 0.0;
339
340 let air_density = calculate_air_density(&self.atmosphere);
342
343 let wind_vector = Vector3::new(
345 self.wind.speed * self.wind.direction.sin(),
346 0.0,
347 self.wind.speed * self.wind.direction.cos(),
348 );
349
350 while position.z < self.max_range && position.y >= 0.0 && time < 100.0 {
352 let velocity_magnitude = velocity.magnitude();
354 let kinetic_energy = 0.5 * self.inputs.mass * velocity_magnitude * velocity_magnitude;
355
356 points.push(TrajectoryPoint {
357 time,
358 position: position.clone(),
359 velocity_magnitude,
360 kinetic_energy,
361 });
362
363 if position.y > max_height {
365 max_height = position.y;
366 }
367
368 if self.inputs.enable_pitch_damping {
370 let temp_c = self.atmosphere.temperature;
371 let temp_k = temp_c + 273.15;
372 let speed_of_sound = (1.4 * 287.05 * temp_k).sqrt();
373 let mach = velocity_magnitude / speed_of_sound;
374
375 if transonic_mach.is_none() && mach < 1.2 && mach > 0.8 {
377 transonic_mach = Some(mach);
378 }
379
380 let bullet_type = if let Some(ref model) = self.inputs.bullet_model {
382 model.as_str()
383 } else {
384 "default"
385 };
386 let coeffs = PitchDampingCoefficients::from_bullet_type(bullet_type);
387 let pitch_damping = calculate_pitch_damping_coefficient(mach, &coeffs);
388
389 if pitch_damping < min_pitch_damping {
391 min_pitch_damping = pitch_damping;
392 }
393 }
394
395 if self.inputs.enable_precession_nutation {
397 if let Some(ref mut state) = angular_state {
398 let velocity_magnitude = velocity.magnitude();
399 let temp_c = self.atmosphere.temperature;
400 let temp_k = temp_c + 273.15;
401 let speed_of_sound = (1.4 * 287.05 * temp_k).sqrt();
402 let mach = velocity_magnitude / speed_of_sound;
403
404 let spin_rate_rad_s = if self.inputs.twist_rate > 0.0 {
406 let velocity_fps = velocity_magnitude * 3.28084;
407 let twist_rate_ft = self.inputs.twist_rate / 12.0;
408 (velocity_fps / twist_rate_ft) * 2.0 * std::f64::consts::PI
409 } else {
410 0.0
411 };
412
413 let params = PrecessionNutationParams {
415 mass_kg: self.inputs.mass,
416 caliber_m: self.inputs.diameter,
417 length_m: self.inputs.bullet_length,
418 spin_rate_rad_s,
419 spin_inertia: 6.94e-8, transverse_inertia: 9.13e-7, velocity_mps: velocity_magnitude,
422 air_density_kg_m3: air_density,
423 mach,
424 pitch_damping_coeff: -0.8,
425 nutation_damping_factor: 0.05,
426 };
427
428 *state = calculate_combined_angular_motion(
430 ¶ms,
431 state,
432 time,
433 self.time_step,
434 0.001, );
436
437 if state.yaw_angle.abs() > max_yaw_angle {
439 max_yaw_angle = state.yaw_angle.abs();
440 }
441 if state.precession_angle.abs() > max_precession_angle {
442 max_precession_angle = state.precession_angle.abs();
443 }
444 }
445 }
446
447 let actual_wind = if self.inputs.enable_wind_shear {
449 self.get_wind_at_altitude(position.y)
450 } else {
451 wind_vector.clone()
452 };
453 let velocity_rel = velocity - actual_wind;
454 let velocity_rel_mag = velocity_rel.magnitude();
455 let drag_coefficient = self.calculate_drag_coefficient(velocity_rel_mag);
456
457 let drag_force = 0.5 * air_density * drag_coefficient *
459 self.inputs.diameter * self.inputs.diameter *
460 std::f64::consts::PI / 4.0 * velocity_rel_mag * velocity_rel_mag;
461
462 let drag_acceleration = -drag_force / self.inputs.mass;
464 let acceleration = Vector3::new(
465 drag_acceleration * velocity_rel.x / velocity_rel_mag,
466 drag_acceleration * velocity_rel.y / velocity_rel_mag - 9.80665,
467 drag_acceleration * velocity_rel.z / velocity_rel_mag,
468 );
469
470 velocity += acceleration * self.time_step;
472 position += velocity * self.time_step;
473 time += self.time_step;
474 }
475
476 let last_point = points.last().ok_or("No trajectory points generated")?;
478
479 let sampled_points = if self.inputs.enable_trajectory_sampling {
481 let trajectory_data = TrajectoryData {
482 times: points.iter().map(|p| p.time).collect(),
483 positions: points.iter().map(|p| p.position.clone()).collect(),
484 velocities: points.iter().map(|p| {
485 Vector3::new(0.0, 0.0, p.velocity_magnitude)
487 }).collect(),
488 transonic_distances: Vec::new(), };
490
491 let outputs = TrajectoryOutputs {
492 target_distance_horiz_m: last_point.position.z,
493 target_vertical_height_m: last_point.position.y,
494 time_of_flight_s: last_point.time,
495 max_ord_dist_horiz_m: max_height,
496 };
497
498 let samples = sample_trajectory(&trajectory_data, &outputs, self.inputs.sample_interval, self.inputs.mass);
500 Some(samples)
501 } else {
502 None
503 };
504
505 Ok(TrajectoryResult {
506 max_range: last_point.position.z,
507 max_height,
508 time_of_flight: last_point.time,
509 impact_velocity: last_point.velocity_magnitude,
510 impact_energy: last_point.kinetic_energy,
511 points,
512 sampled_points,
513 min_pitch_damping: if self.inputs.enable_pitch_damping { Some(min_pitch_damping) } else { None },
514 transonic_mach: transonic_mach,
515 angular_state: angular_state,
516 max_yaw_angle: if self.inputs.enable_precession_nutation { Some(max_yaw_angle) } else { None },
517 max_precession_angle: if self.inputs.enable_precession_nutation { Some(max_precession_angle) } else { None },
518 })
519 }
520
521 fn solve_rk4(&self) -> Result<TrajectoryResult, BallisticsError> {
522 let mut time = 0.0;
524 let mut position = Vector3::new(0.0, self.inputs.sight_height, 0.0);
525
526 let horizontal_velocity = self.inputs.muzzle_velocity * self.inputs.launch_angle.cos();
528 let mut velocity = Vector3::new(
529 horizontal_velocity * self.inputs.azimuth_angle.sin(),
530 self.inputs.muzzle_velocity * self.inputs.launch_angle.sin(),
531 horizontal_velocity * self.inputs.azimuth_angle.cos(),
532 );
533
534 let mut points = Vec::new();
535 let mut max_height = position.y;
536 let mut min_pitch_damping = 1.0; let mut transonic_mach = None; let mut angular_state = if self.inputs.enable_precession_nutation {
541 Some(AngularState {
542 pitch_angle: 0.001, yaw_angle: 0.001,
544 pitch_rate: 0.0,
545 yaw_rate: 0.0,
546 precession_angle: 0.0,
547 nutation_phase: 0.0,
548 })
549 } else {
550 None
551 };
552 let mut max_yaw_angle = 0.0;
553 let mut max_precession_angle = 0.0;
554
555 let air_density = calculate_air_density(&self.atmosphere);
557
558 let wind_vector = Vector3::new(
560 self.wind.speed * self.wind.direction.sin(),
561 0.0,
562 self.wind.speed * self.wind.direction.cos(),
563 );
564
565 while position.z < self.max_range && position.y >= 0.0 && time < 100.0 {
567 let velocity_magnitude = velocity.magnitude();
569 let kinetic_energy = 0.5 * self.inputs.mass * velocity_magnitude * velocity_magnitude;
570
571 points.push(TrajectoryPoint {
572 time,
573 position: position.clone(),
574 velocity_magnitude,
575 kinetic_energy,
576 });
577
578 if position.y > max_height {
579 max_height = position.y;
580 }
581
582 if self.inputs.enable_pitch_damping {
584 let temp_c = self.atmosphere.temperature;
585 let temp_k = temp_c + 273.15;
586 let speed_of_sound = (1.4 * 287.05 * temp_k).sqrt();
587 let mach = velocity_magnitude / speed_of_sound;
588
589 if transonic_mach.is_none() && mach < 1.2 && mach > 0.8 {
591 transonic_mach = Some(mach);
592 }
593
594 let bullet_type = if let Some(ref model) = self.inputs.bullet_model {
596 model.as_str()
597 } else {
598 "default"
599 };
600 let coeffs = PitchDampingCoefficients::from_bullet_type(bullet_type);
601 let pitch_damping = calculate_pitch_damping_coefficient(mach, &coeffs);
602
603 if pitch_damping < min_pitch_damping {
605 min_pitch_damping = pitch_damping;
606 }
607 }
608
609 if self.inputs.enable_precession_nutation {
611 if let Some(ref mut state) = angular_state {
612 let velocity_magnitude = velocity.magnitude();
613 let temp_c = self.atmosphere.temperature;
614 let temp_k = temp_c + 273.15;
615 let speed_of_sound = (1.4 * 287.05 * temp_k).sqrt();
616 let mach = velocity_magnitude / speed_of_sound;
617
618 let spin_rate_rad_s = if self.inputs.twist_rate > 0.0 {
620 let velocity_fps = velocity_magnitude * 3.28084;
621 let twist_rate_ft = self.inputs.twist_rate / 12.0;
622 (velocity_fps / twist_rate_ft) * 2.0 * std::f64::consts::PI
623 } else {
624 0.0
625 };
626
627 let params = PrecessionNutationParams {
629 mass_kg: self.inputs.mass,
630 caliber_m: self.inputs.diameter,
631 length_m: self.inputs.bullet_length,
632 spin_rate_rad_s,
633 spin_inertia: 6.94e-8, transverse_inertia: 9.13e-7, velocity_mps: velocity_magnitude,
636 air_density_kg_m3: air_density,
637 mach,
638 pitch_damping_coeff: -0.8,
639 nutation_damping_factor: 0.05,
640 };
641
642 *state = calculate_combined_angular_motion(
644 ¶ms,
645 state,
646 time,
647 self.time_step,
648 0.001, );
650
651 if state.yaw_angle.abs() > max_yaw_angle {
653 max_yaw_angle = state.yaw_angle.abs();
654 }
655 if state.precession_angle.abs() > max_precession_angle {
656 max_precession_angle = state.precession_angle.abs();
657 }
658 }
659 }
660
661 let dt = self.time_step;
663
664 let acc1 = self.calculate_acceleration(&position, &velocity, air_density, &wind_vector);
666
667 let pos2 = position + velocity * (dt * 0.5);
669 let vel2 = velocity + acc1 * (dt * 0.5);
670 let acc2 = self.calculate_acceleration(&pos2, &vel2, air_density, &wind_vector);
671
672 let pos3 = position + vel2 * (dt * 0.5);
674 let vel3 = velocity + acc2 * (dt * 0.5);
675 let acc3 = self.calculate_acceleration(&pos3, &vel3, air_density, &wind_vector);
676
677 let pos4 = position + vel3 * dt;
679 let vel4 = velocity + acc3 * dt;
680 let acc4 = self.calculate_acceleration(&pos4, &vel4, air_density, &wind_vector);
681
682 position += (velocity + vel2 * 2.0 + vel3 * 2.0 + vel4) * (dt / 6.0);
684 velocity += (acc1 + acc2 * 2.0 + acc3 * 2.0 + acc4) * (dt / 6.0);
685 time += dt;
686 }
687
688 let last_point = points.last().ok_or("No trajectory points generated")?;
690
691 let sampled_points = if self.inputs.enable_trajectory_sampling {
693 let trajectory_data = TrajectoryData {
694 times: points.iter().map(|p| p.time).collect(),
695 positions: points.iter().map(|p| p.position.clone()).collect(),
696 velocities: points.iter().map(|p| {
697 Vector3::new(0.0, 0.0, p.velocity_magnitude)
699 }).collect(),
700 transonic_distances: Vec::new(), };
702
703 let outputs = TrajectoryOutputs {
704 target_distance_horiz_m: last_point.position.z,
705 target_vertical_height_m: last_point.position.y,
706 time_of_flight_s: last_point.time,
707 max_ord_dist_horiz_m: max_height,
708 };
709
710 let samples = sample_trajectory(&trajectory_data, &outputs, self.inputs.sample_interval, self.inputs.mass);
712 Some(samples)
713 } else {
714 None
715 };
716
717 Ok(TrajectoryResult {
718 max_range: last_point.position.z,
719 max_height,
720 time_of_flight: last_point.time,
721 impact_velocity: last_point.velocity_magnitude,
722 impact_energy: last_point.kinetic_energy,
723 points,
724 sampled_points,
725 min_pitch_damping: if self.inputs.enable_pitch_damping { Some(min_pitch_damping) } else { None },
726 transonic_mach: transonic_mach,
727 angular_state: angular_state,
728 max_yaw_angle: if self.inputs.enable_precession_nutation { Some(max_yaw_angle) } else { None },
729 max_precession_angle: if self.inputs.enable_precession_nutation { Some(max_precession_angle) } else { None },
730 })
731 }
732
733 fn calculate_acceleration(&self, position: &Vector3<f64>, velocity: &Vector3<f64>, air_density: f64, wind_vector: &Vector3<f64>) -> Vector3<f64> {
734 let actual_wind = if self.inputs.enable_wind_shear {
736 self.get_wind_at_altitude(position.y)
737 } else {
738 wind_vector.clone()
739 };
740
741 let relative_velocity = velocity - &actual_wind;
742 let velocity_magnitude = relative_velocity.magnitude();
743
744 if velocity_magnitude < 0.001 {
745 return Vector3::new(0.0, -9.81, 0.0);
746 }
747
748 let cd = self.calculate_drag_coefficient(velocity_magnitude);
750 let reference_area = std::f64::consts::PI * (self.inputs.diameter / 2.0).powi(2);
751 let drag_magnitude = 0.5 * air_density * velocity_magnitude.powi(2) * cd * reference_area / self.inputs.ballistic_coefficient;
752
753 let drag_force = -relative_velocity.normalize() * drag_magnitude;
755
756 let acceleration = drag_force / self.inputs.mass + Vector3::new(0.0, -9.81, 0.0);
758
759 acceleration
760 }
761
762 fn calculate_drag_coefficient(&self, velocity: f64) -> f64 {
763 let temp_c = self.atmosphere.temperature;
766 let temp_k = temp_c + 273.15;
767 let gamma = 1.4; let r_specific = 287.05; let speed_of_sound = (gamma * r_specific * temp_k).sqrt();
770 let mach = velocity / speed_of_sound;
771
772 let base_cd = match self.inputs.drag_model {
774 DragModel::G1 => 0.5,
775 DragModel::G7 => 0.4,
776 _ => 0.45,
777 };
778
779 let projectile_shape = if let Some(ref model) = self.inputs.bullet_model {
781 if model.to_lowercase().contains("boat") || model.to_lowercase().contains("bt") {
783 ProjectileShape::BoatTail
784 } else if model.to_lowercase().contains("round") || model.to_lowercase().contains("rn") {
785 ProjectileShape::RoundNose
786 } else if model.to_lowercase().contains("flat") || model.to_lowercase().contains("fb") {
787 ProjectileShape::FlatBase
788 } else {
789 get_projectile_shape(
791 self.inputs.diameter,
792 self.inputs.mass / 0.00006479891, &self.inputs.drag_model.to_string()
794 )
795 }
796 } else {
797 get_projectile_shape(
799 self.inputs.diameter,
800 self.inputs.mass / 0.00006479891, &self.inputs.drag_model.to_string()
802 )
803 };
804
805 let include_wave_drag = self.inputs.enable_advanced_effects;
808 transonic_correction(mach, base_cd, projectile_shape, include_wave_drag)
809 }
810}
811
812#[derive(Debug, Clone)]
814pub struct MonteCarloParams {
815 pub num_simulations: usize,
816 pub velocity_std_dev: f64,
817 pub angle_std_dev: f64,
818 pub bc_std_dev: f64,
819 pub wind_speed_std_dev: f64,
820 pub target_distance: Option<f64>,
821 pub base_wind_speed: f64,
822 pub base_wind_direction: f64,
823 pub azimuth_std_dev: f64, }
825
826impl Default for MonteCarloParams {
827 fn default() -> Self {
828 Self {
829 num_simulations: 1000,
830 velocity_std_dev: 1.0,
831 angle_std_dev: 0.001,
832 bc_std_dev: 0.01,
833 wind_speed_std_dev: 1.0,
834 target_distance: None,
835 base_wind_speed: 0.0,
836 base_wind_direction: 0.0,
837 azimuth_std_dev: 0.001, }
839 }
840}
841
842#[derive(Debug, Clone)]
844pub struct MonteCarloResults {
845 pub ranges: Vec<f64>,
846 pub impact_velocities: Vec<f64>,
847 pub impact_positions: Vec<Vector3<f64>>,
848}
849
850pub fn run_monte_carlo(
852 base_inputs: BallisticInputs,
853 params: MonteCarloParams,
854) -> Result<MonteCarloResults, BallisticsError> {
855 let base_wind = WindConditions {
856 speed: params.base_wind_speed,
857 direction: params.base_wind_direction,
858 };
859 run_monte_carlo_with_wind(base_inputs, base_wind, params)
860}
861
862pub fn run_monte_carlo_with_wind(
864 base_inputs: BallisticInputs,
865 base_wind: WindConditions,
866 params: MonteCarloParams,
867) -> Result<MonteCarloResults, BallisticsError> {
868 use rand::{thread_rng, Rng};
869 use rand_distr::{Distribution, Normal};
870
871 let mut rng = thread_rng();
872 let mut ranges = Vec::new();
873 let mut impact_velocities = Vec::new();
874 let mut impact_positions = Vec::new();
875
876 let baseline_solver = TrajectorySolver::new(base_inputs.clone(), base_wind.clone(), Default::default());
878 let baseline_result = baseline_solver.solve()?;
879 let baseline_impact = baseline_result.points.last()
880 .ok_or("No baseline trajectory points")?
881 .position.clone();
882
883 let velocity_dist = Normal::new(base_inputs.muzzle_velocity, params.velocity_std_dev)
885 .map_err(|e| format!("Invalid velocity distribution: {}", e))?;
886 let angle_dist = Normal::new(base_inputs.launch_angle, params.angle_std_dev)
887 .map_err(|e| format!("Invalid angle distribution: {}", e))?;
888 let bc_dist = Normal::new(base_inputs.ballistic_coefficient, params.bc_std_dev)
889 .map_err(|e| format!("Invalid BC distribution: {}", e))?;
890 let wind_speed_dist = Normal::new(base_wind.speed, params.wind_speed_std_dev)
891 .map_err(|e| format!("Invalid wind speed distribution: {}", e))?;
892 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))?;
894 let azimuth_dist = Normal::new(base_inputs.azimuth_angle, params.azimuth_std_dev)
895 .map_err(|e| format!("Invalid azimuth distribution: {}", e))?;
896
897 let distance = baseline_impact.z; let pointing_error_dist = Normal::new(0.0, params.angle_std_dev * distance)
900 .map_err(|e| format!("Invalid pointing distribution: {}", e))?;
901
902 for _ in 0..params.num_simulations {
903 let mut inputs = base_inputs.clone();
905 inputs.muzzle_velocity = velocity_dist.sample(&mut rng).max(0.0);
906 inputs.launch_angle = angle_dist.sample(&mut rng);
907 inputs.ballistic_coefficient = bc_dist.sample(&mut rng).max(0.01);
908 inputs.azimuth_angle = azimuth_dist.sample(&mut rng); let wind = WindConditions {
912 speed: wind_speed_dist.sample(&mut rng).abs(),
913 direction: wind_dir_dist.sample(&mut rng),
914 };
915
916 let solver = TrajectorySolver::new(inputs, wind, Default::default());
918 match solver.solve() {
919 Ok(result) => {
920 ranges.push(result.max_range);
921 impact_velocities.push(result.impact_velocity);
922 if let Some(last_point) = result.points.last() {
923 let mut deviation = Vector3::new(
925 last_point.position.x - baseline_impact.x, last_point.position.y - baseline_impact.y, last_point.position.z - baseline_impact.z, );
929
930 let pointing_error_y = pointing_error_dist.sample(&mut rng);
933 deviation.y += pointing_error_y;
934
935 impact_positions.push(deviation);
936 }
937 },
938 Err(_) => {
939 continue;
941 }
942 }
943 }
944
945 if ranges.is_empty() {
946 return Err("No successful simulations".into());
947 }
948
949 Ok(MonteCarloResults {
950 ranges,
951 impact_velocities,
952 impact_positions,
953 })
954}
955
956pub fn calculate_zero_angle(
958 inputs: BallisticInputs,
959 target_distance: f64,
960 target_height: f64,
961) -> Result<f64, BallisticsError> {
962 calculate_zero_angle_with_conditions(
963 inputs,
964 target_distance,
965 target_height,
966 WindConditions::default(),
967 AtmosphericConditions::default(),
968 )
969}
970
971pub fn calculate_zero_angle_with_conditions(
972 inputs: BallisticInputs,
973 target_distance: f64,
974 target_height: f64,
975 wind: WindConditions,
976 atmosphere: AtmosphericConditions,
977) -> Result<f64, BallisticsError> {
978 let mut low_angle = -0.2; let mut high_angle = 0.2; let tolerance = 0.00001; let max_iterations = 50;
983
984 for iteration in 0..max_iterations {
985 let mid_angle = (low_angle + high_angle) / 2.0;
986
987 let mut test_inputs = inputs.clone();
988 test_inputs.launch_angle = mid_angle;
989
990 let mut solver = TrajectorySolver::new(test_inputs, wind.clone(), atmosphere.clone());
991 solver.set_max_range(target_distance * 2.0);
993 solver.set_time_step(0.001);
994 let result = solver.solve()?;
995
996 eprintln!(" Iteration {}: angle = {:.6} rad ({:.4} deg)",
997 iteration, mid_angle, mid_angle * 180.0 / std::f64::consts::PI);
998
999 let mut height_at_target = None;
1001 for i in 0..result.points.len() {
1002 if result.points[i].position.z >= target_distance {
1003 if i > 0 {
1004 let p1 = &result.points[i - 1];
1006 let p2 = &result.points[i];
1007 let t = (target_distance - p1.position.z) / (p2.position.z - p1.position.z);
1008 height_at_target = Some(p1.position.y + t * (p2.position.y - p1.position.y));
1009 } else {
1010 height_at_target = Some(result.points[i].position.y);
1011 }
1012 break;
1013 }
1014 }
1015
1016 match height_at_target {
1017 Some(height) => {
1018 let error = height - target_height;
1019 eprintln!(" Height at target: {:.6} m, target: {:.6} m, error: {:.6} m",
1020 height, target_height, error);
1021 if error.abs() < 0.001 {
1022 eprintln!(" Converged!");
1023 return Ok(mid_angle);
1024 }
1025
1026 if error > 0.0 {
1027 high_angle = mid_angle;
1028 } else {
1029 low_angle = mid_angle;
1030 }
1031 },
1032 None => {
1033 low_angle = mid_angle;
1035 }
1036 }
1037
1038 if (high_angle - low_angle).abs() < tolerance {
1039 return Ok(mid_angle);
1040 }
1041 }
1042
1043 Err("Failed to find zero angle".into())
1044}
1045
1046pub fn estimate_bc_from_trajectory(
1048 velocity: f64,
1049 mass: f64,
1050 diameter: f64,
1051 points: &[(f64, f64)], ) -> Result<f64, BallisticsError> {
1053 let mut best_bc = 0.5;
1055 let mut best_error = f64::MAX;
1056 let mut found_valid = false;
1057
1058 for bc in (100..1000).step_by(10) {
1060 let bc_value = bc as f64 / 1000.0;
1061
1062 let inputs = BallisticInputs {
1063 muzzle_velocity: velocity,
1064 ballistic_coefficient: bc_value,
1065 mass,
1066 diameter,
1067 ..Default::default()
1068 };
1069
1070 let mut solver = TrajectorySolver::new(inputs, Default::default(), Default::default());
1071 solver.set_max_range(points.last().map(|(d, _)| *d * 1.5).unwrap_or(1000.0));
1073
1074 let result = match solver.solve() {
1075 Ok(r) => r,
1076 Err(_) => continue, };
1078
1079 let mut total_error = 0.0;
1081 for (target_dist, target_drop) in points {
1082 let mut calculated_drop = None;
1084 for i in 0..result.points.len() {
1085 if result.points[i].position.z >= *target_dist {
1086 if i > 0 {
1087 let p1 = &result.points[i - 1];
1089 let p2 = &result.points[i];
1090 let t = (target_dist - p1.position.z) / (p2.position.z - p1.position.z);
1091 calculated_drop = Some(-(p1.position.y + t * (p2.position.y - p1.position.y)));
1092 } else {
1093 calculated_drop = Some(-result.points[i].position.y);
1094 }
1095 break;
1096 }
1097 }
1098
1099 if let Some(drop) = calculated_drop {
1100 let error = (drop - target_drop).abs();
1101 total_error += error * error;
1102 }
1103 }
1104
1105 if total_error < best_error {
1106 best_error = total_error;
1107 best_bc = bc_value;
1108 found_valid = true;
1109 }
1110 }
1111
1112 if !found_valid {
1113 return Err(BallisticsError::from("Unable to estimate BC from provided data. Check that drop values are in correct units.".to_string()));
1114 }
1115
1116 Ok(best_bc)
1117}
1118
1119fn calculate_air_density(atmosphere: &AtmosphericConditions) -> f64 {
1121 let r_specific = 287.058; let temperature_k = atmosphere.temperature + 273.15;
1125
1126 let pressure_pa = atmosphere.pressure * 100.0;
1128
1129 let density = pressure_pa / (r_specific * temperature_k);
1131
1132 let altitude_factor = (-atmosphere.altitude / 8000.0).exp();
1134
1135 density * altitude_factor
1136}
1137
1138use rand;
1140use rand_distr;