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)]
14pub struct BallisticsError {
15 message: String,
16}
17
18impl fmt::Display for BallisticsError {
19 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
20 write!(f, "{}", self.message)
21 }
22}
23
24impl Error for BallisticsError {}
25
26impl From<String> for BallisticsError {
27 fn from(msg: String) -> Self {
28 BallisticsError { message: msg }
29 }
30}
31
32impl From<&str> for BallisticsError {
33 fn from(msg: &str) -> Self {
34 BallisticsError { message: msg.to_string() }
35 }
36}
37
38#[derive(Debug, Clone)]
40pub struct BallisticInputs {
41 pub muzzle_velocity: f64, pub launch_angle: f64, pub ballistic_coefficient: f64,
45 pub mass: f64, pub diameter: f64, pub drag_model: DragModel,
48 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,
72 pub use_powder_sensitivity: bool,
73 pub powder_temp_sensitivity: f64,
74 pub powder_temp: f64, pub tipoff_yaw: f64, pub tipoff_decay_distance: f64, pub use_bc_segments: bool,
78 pub bc_segments: Option<Vec<(f64, f64)>>, pub bc_segments_data: Option<Vec<crate::BCSegmentData>>, pub use_enhanced_spin_drift: bool,
81 pub use_form_factor: bool,
82 pub enable_wind_shear: bool,
83 pub wind_shear_model: String,
84 pub enable_trajectory_sampling: bool,
85 pub sample_interval: f64, pub enable_pitch_damping: bool,
87 pub enable_precession_nutation: bool,
88
89 pub bc_type_str: Option<String>,
91 pub bullet_model: Option<String>,
92 pub bullet_id: Option<String>,
93}
94
95impl Default for BallisticInputs {
96 fn default() -> Self {
97 let mass_kg = 0.01;
98 let diameter_m = 0.00762;
99 let bc = 0.5;
100 let launch_angle_rad = 0.0;
101 let drag_model = DragModel::G1;
102
103 Self {
104 muzzle_velocity: 800.0,
106 launch_angle: launch_angle_rad,
107 ballistic_coefficient: bc,
108 mass: mass_kg,
109 diameter: diameter_m,
110 drag_model,
111 sight_height: 0.05,
112
113 altitude: 0.0,
115 bc_type: drag_model,
116 bc_value: bc,
117 caliber_inches: diameter_m / 0.0254, weight_grains: mass_kg / 0.00006479891, bullet_diameter: diameter_m,
120 bullet_mass: mass_kg,
121 bullet_length: diameter_m * 4.0, muzzle_angle: launch_angle_rad,
123 target_distance: 100.0,
124 azimuth_angle: 0.0,
125 use_rk4: true, temperature: 15.0,
127 twist_rate: 12.0, is_twist_right: true,
129 shooting_angle: 0.0,
130 latitude: None,
131 ground_threshold: -10.0,
132
133 enable_advanced_effects: false,
135 use_powder_sensitivity: false,
136 powder_temp_sensitivity: 0.0,
137 powder_temp: 15.0,
138 tipoff_yaw: 0.0,
139 tipoff_decay_distance: 50.0,
140 use_bc_segments: false,
141 bc_segments: None,
142 bc_segments_data: None,
143 use_enhanced_spin_drift: false,
144 use_form_factor: false,
145 enable_wind_shear: false,
146 wind_shear_model: "none".to_string(),
147 enable_trajectory_sampling: false,
148 sample_interval: 10.0, enable_pitch_damping: false,
150 enable_precession_nutation: false,
151
152 bc_type_str: None,
154 bullet_model: None,
155 bullet_id: None,
156 }
157 }
158}
159
160#[derive(Debug, Clone)]
162pub struct WindConditions {
163 pub speed: f64, pub direction: f64, }
166
167impl Default for WindConditions {
168 fn default() -> Self {
169 Self {
170 speed: 0.0,
171 direction: 0.0,
172 }
173 }
174}
175
176#[derive(Debug, Clone)]
178pub struct AtmosphericConditions {
179 pub temperature: f64, pub pressure: f64, pub humidity: f64, pub altitude: f64, }
184
185impl Default for AtmosphericConditions {
186 fn default() -> Self {
187 Self {
188 temperature: 15.0,
189 pressure: 1013.25,
190 humidity: 50.0,
191 altitude: 0.0,
192 }
193 }
194}
195
196#[derive(Debug, Clone)]
198pub struct TrajectoryPoint {
199 pub time: f64,
200 pub position: Vector3<f64>,
201 pub velocity_magnitude: f64,
202 pub kinetic_energy: f64,
203}
204
205#[derive(Debug, Clone)]
207pub struct TrajectoryResult {
208 pub max_range: f64,
209 pub max_height: f64,
210 pub time_of_flight: f64,
211 pub impact_velocity: f64,
212 pub impact_energy: f64,
213 pub points: Vec<TrajectoryPoint>,
214 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>, }
221
222pub struct TrajectorySolver {
224 inputs: BallisticInputs,
225 wind: WindConditions,
226 atmosphere: AtmosphericConditions,
227 max_range: f64,
228 time_step: f64,
229}
230
231impl TrajectorySolver {
232 pub fn new(mut inputs: BallisticInputs, wind: WindConditions, atmosphere: AtmosphericConditions) -> Self {
233 inputs.bc_type = inputs.drag_model;
235 inputs.bc_value = inputs.ballistic_coefficient;
236 inputs.bullet_diameter = inputs.diameter;
237 inputs.bullet_mass = inputs.mass;
238 inputs.muzzle_angle = inputs.launch_angle;
239 inputs.caliber_inches = inputs.diameter / 0.0254;
240 inputs.weight_grains = inputs.mass / 0.00006479891;
241
242 Self {
243 inputs,
244 wind,
245 atmosphere,
246 max_range: 1000.0,
247 time_step: 0.001,
248 }
249 }
250
251 pub fn set_max_range(&mut self, range: f64) {
252 self.max_range = range;
253 }
254
255 pub fn set_time_step(&mut self, step: f64) {
256 self.time_step = step;
257 }
258
259 fn get_wind_at_altitude(&self, altitude_m: f64) -> Vector3<f64> {
260 let profile = WindShearProfile {
262 model: if self.inputs.wind_shear_model == "logarithmic" {
263 WindShearModel::Logarithmic
264 } else if self.inputs.wind_shear_model == "power" {
265 WindShearModel::PowerLaw
266 } else {
267 WindShearModel::PowerLaw },
269 surface_wind: WindLayer {
270 altitude_m: 0.0,
271 speed_mps: self.wind.speed,
272 direction_deg: self.wind.direction.to_degrees(),
273 },
274 reference_height: 10.0, roughness_length: 0.03, power_exponent: 1.0 / 7.0, geostrophic_wind: None,
278 custom_layers: Vec::new(),
279 };
280
281 profile.get_wind_at_altitude(altitude_m)
282 }
283
284 pub fn solve(&self) -> Result<TrajectoryResult, BallisticsError> {
285 if self.inputs.use_rk4 {
286 self.solve_rk4()
287 } else {
288 self.solve_euler()
289 }
290 }
291
292 fn solve_euler(&self) -> Result<TrajectoryResult, BallisticsError> {
293 let mut time = 0.0;
295 let mut position = Vector3::new(0.0, self.inputs.sight_height, 0.0);
296 let horizontal_velocity = self.inputs.muzzle_velocity * self.inputs.launch_angle.cos();
298 let mut velocity = Vector3::new(
299 horizontal_velocity * self.inputs.azimuth_angle.sin(), self.inputs.muzzle_velocity * self.inputs.launch_angle.sin(), horizontal_velocity * self.inputs.azimuth_angle.cos(), );
303
304 let mut points = Vec::new();
305 let mut max_height = position.y;
306 let mut min_pitch_damping = 1.0; let mut transonic_mach = None; let mut angular_state = if self.inputs.enable_precession_nutation {
311 Some(AngularState {
312 pitch_angle: 0.001, yaw_angle: 0.001,
314 pitch_rate: 0.0,
315 yaw_rate: 0.0,
316 precession_angle: 0.0,
317 nutation_phase: 0.0,
318 })
319 } else {
320 None
321 };
322 let mut max_yaw_angle = 0.0;
323 let mut max_precession_angle = 0.0;
324
325 let air_density = calculate_air_density(&self.atmosphere);
327
328 let wind_vector = Vector3::new(
330 self.wind.speed * self.wind.direction.sin(),
331 0.0,
332 self.wind.speed * self.wind.direction.cos(),
333 );
334
335 while position.z < self.max_range && position.y >= 0.0 && time < 100.0 {
337 let velocity_magnitude = velocity.magnitude();
339 let kinetic_energy = 0.5 * self.inputs.mass * velocity_magnitude * velocity_magnitude;
340
341 points.push(TrajectoryPoint {
342 time,
343 position: position.clone(),
344 velocity_magnitude,
345 kinetic_energy,
346 });
347
348 if position.y > max_height {
350 max_height = position.y;
351 }
352
353 if self.inputs.enable_pitch_damping {
355 let temp_c = self.atmosphere.temperature;
356 let temp_k = temp_c + 273.15;
357 let speed_of_sound = (1.4 * 287.05 * temp_k).sqrt();
358 let mach = velocity_magnitude / speed_of_sound;
359
360 if transonic_mach.is_none() && mach < 1.2 && mach > 0.8 {
362 transonic_mach = Some(mach);
363 }
364
365 let bullet_type = if let Some(ref model) = self.inputs.bullet_model {
367 model.as_str()
368 } else {
369 "default"
370 };
371 let coeffs = PitchDampingCoefficients::from_bullet_type(bullet_type);
372 let pitch_damping = calculate_pitch_damping_coefficient(mach, &coeffs);
373
374 if pitch_damping < min_pitch_damping {
376 min_pitch_damping = pitch_damping;
377 }
378 }
379
380 if self.inputs.enable_precession_nutation {
382 if let Some(ref mut state) = angular_state {
383 let velocity_magnitude = velocity.magnitude();
384 let temp_c = self.atmosphere.temperature;
385 let temp_k = temp_c + 273.15;
386 let speed_of_sound = (1.4 * 287.05 * temp_k).sqrt();
387 let mach = velocity_magnitude / speed_of_sound;
388
389 let spin_rate_rad_s = if self.inputs.twist_rate > 0.0 {
391 let velocity_fps = velocity_magnitude * 3.28084;
392 let twist_rate_ft = self.inputs.twist_rate / 12.0;
393 (velocity_fps / twist_rate_ft) * 2.0 * std::f64::consts::PI
394 } else {
395 0.0
396 };
397
398 let params = PrecessionNutationParams {
400 mass_kg: self.inputs.mass,
401 caliber_m: self.inputs.diameter,
402 length_m: self.inputs.bullet_length,
403 spin_rate_rad_s,
404 spin_inertia: 6.94e-8, transverse_inertia: 9.13e-7, velocity_mps: velocity_magnitude,
407 air_density_kg_m3: air_density,
408 mach,
409 pitch_damping_coeff: -0.8,
410 nutation_damping_factor: 0.05,
411 };
412
413 *state = calculate_combined_angular_motion(
415 ¶ms,
416 state,
417 time,
418 self.time_step,
419 0.001, );
421
422 if state.yaw_angle.abs() > max_yaw_angle {
424 max_yaw_angle = state.yaw_angle.abs();
425 }
426 if state.precession_angle.abs() > max_precession_angle {
427 max_precession_angle = state.precession_angle.abs();
428 }
429 }
430 }
431
432 let actual_wind = if self.inputs.enable_wind_shear {
434 self.get_wind_at_altitude(position.y)
435 } else {
436 wind_vector.clone()
437 };
438 let velocity_rel = velocity - actual_wind;
439 let velocity_rel_mag = velocity_rel.magnitude();
440 let drag_coefficient = self.calculate_drag_coefficient(velocity_rel_mag);
441
442 let drag_force = 0.5 * air_density * drag_coefficient *
444 self.inputs.diameter * self.inputs.diameter *
445 std::f64::consts::PI / 4.0 * velocity_rel_mag * velocity_rel_mag;
446
447 let drag_acceleration = -drag_force / self.inputs.mass;
449 let acceleration = Vector3::new(
450 drag_acceleration * velocity_rel.x / velocity_rel_mag,
451 drag_acceleration * velocity_rel.y / velocity_rel_mag - 9.80665,
452 drag_acceleration * velocity_rel.z / velocity_rel_mag,
453 );
454
455 velocity += acceleration * self.time_step;
457 position += velocity * self.time_step;
458 time += self.time_step;
459 }
460
461 let last_point = points.last().ok_or("No trajectory points generated")?;
463
464 let sampled_points = if self.inputs.enable_trajectory_sampling {
466 let trajectory_data = TrajectoryData {
467 times: points.iter().map(|p| p.time).collect(),
468 positions: points.iter().map(|p| p.position.clone()).collect(),
469 velocities: points.iter().map(|p| {
470 Vector3::new(0.0, 0.0, p.velocity_magnitude)
472 }).collect(),
473 transonic_distances: Vec::new(), };
475
476 let outputs = TrajectoryOutputs {
477 target_distance_horiz_m: last_point.position.z,
478 target_vertical_height_m: last_point.position.y,
479 time_of_flight_s: last_point.time,
480 max_ord_dist_horiz_m: max_height,
481 };
482
483 let samples = sample_trajectory(&trajectory_data, &outputs, self.inputs.sample_interval, self.inputs.mass);
485 Some(samples)
486 } else {
487 None
488 };
489
490 Ok(TrajectoryResult {
491 max_range: last_point.position.z,
492 max_height,
493 time_of_flight: last_point.time,
494 impact_velocity: last_point.velocity_magnitude,
495 impact_energy: last_point.kinetic_energy,
496 points,
497 sampled_points,
498 min_pitch_damping: if self.inputs.enable_pitch_damping { Some(min_pitch_damping) } else { None },
499 transonic_mach: transonic_mach,
500 angular_state: angular_state,
501 max_yaw_angle: if self.inputs.enable_precession_nutation { Some(max_yaw_angle) } else { None },
502 max_precession_angle: if self.inputs.enable_precession_nutation { Some(max_precession_angle) } else { None },
503 })
504 }
505
506 fn solve_rk4(&self) -> Result<TrajectoryResult, BallisticsError> {
507 let mut time = 0.0;
509 let mut position = Vector3::new(0.0, self.inputs.sight_height, 0.0);
510
511 let horizontal_velocity = self.inputs.muzzle_velocity * self.inputs.launch_angle.cos();
513 let mut velocity = Vector3::new(
514 horizontal_velocity * self.inputs.azimuth_angle.sin(),
515 self.inputs.muzzle_velocity * self.inputs.launch_angle.sin(),
516 horizontal_velocity * self.inputs.azimuth_angle.cos(),
517 );
518
519 let mut points = Vec::new();
520 let mut max_height = position.y;
521 let mut min_pitch_damping = 1.0; let mut transonic_mach = None; let mut angular_state = if self.inputs.enable_precession_nutation {
526 Some(AngularState {
527 pitch_angle: 0.001, yaw_angle: 0.001,
529 pitch_rate: 0.0,
530 yaw_rate: 0.0,
531 precession_angle: 0.0,
532 nutation_phase: 0.0,
533 })
534 } else {
535 None
536 };
537 let mut max_yaw_angle = 0.0;
538 let mut max_precession_angle = 0.0;
539
540 let air_density = calculate_air_density(&self.atmosphere);
542
543 let wind_vector = Vector3::new(
545 self.wind.speed * self.wind.direction.sin(),
546 0.0,
547 self.wind.speed * self.wind.direction.cos(),
548 );
549
550 while position.z < self.max_range && position.y >= 0.0 && time < 100.0 {
552 let velocity_magnitude = velocity.magnitude();
554 let kinetic_energy = 0.5 * self.inputs.mass * velocity_magnitude * velocity_magnitude;
555
556 points.push(TrajectoryPoint {
557 time,
558 position: position.clone(),
559 velocity_magnitude,
560 kinetic_energy,
561 });
562
563 if position.y > max_height {
564 max_height = position.y;
565 }
566
567 if self.inputs.enable_pitch_damping {
569 let temp_c = self.atmosphere.temperature;
570 let temp_k = temp_c + 273.15;
571 let speed_of_sound = (1.4 * 287.05 * temp_k).sqrt();
572 let mach = velocity_magnitude / speed_of_sound;
573
574 if transonic_mach.is_none() && mach < 1.2 && mach > 0.8 {
576 transonic_mach = Some(mach);
577 }
578
579 let bullet_type = if let Some(ref model) = self.inputs.bullet_model {
581 model.as_str()
582 } else {
583 "default"
584 };
585 let coeffs = PitchDampingCoefficients::from_bullet_type(bullet_type);
586 let pitch_damping = calculate_pitch_damping_coefficient(mach, &coeffs);
587
588 if pitch_damping < min_pitch_damping {
590 min_pitch_damping = pitch_damping;
591 }
592 }
593
594 if self.inputs.enable_precession_nutation {
596 if let Some(ref mut state) = angular_state {
597 let velocity_magnitude = velocity.magnitude();
598 let temp_c = self.atmosphere.temperature;
599 let temp_k = temp_c + 273.15;
600 let speed_of_sound = (1.4 * 287.05 * temp_k).sqrt();
601 let mach = velocity_magnitude / speed_of_sound;
602
603 let spin_rate_rad_s = if self.inputs.twist_rate > 0.0 {
605 let velocity_fps = velocity_magnitude * 3.28084;
606 let twist_rate_ft = self.inputs.twist_rate / 12.0;
607 (velocity_fps / twist_rate_ft) * 2.0 * std::f64::consts::PI
608 } else {
609 0.0
610 };
611
612 let params = PrecessionNutationParams {
614 mass_kg: self.inputs.mass,
615 caliber_m: self.inputs.diameter,
616 length_m: self.inputs.bullet_length,
617 spin_rate_rad_s,
618 spin_inertia: 6.94e-8, transverse_inertia: 9.13e-7, velocity_mps: velocity_magnitude,
621 air_density_kg_m3: air_density,
622 mach,
623 pitch_damping_coeff: -0.8,
624 nutation_damping_factor: 0.05,
625 };
626
627 *state = calculate_combined_angular_motion(
629 ¶ms,
630 state,
631 time,
632 self.time_step,
633 0.001, );
635
636 if state.yaw_angle.abs() > max_yaw_angle {
638 max_yaw_angle = state.yaw_angle.abs();
639 }
640 if state.precession_angle.abs() > max_precession_angle {
641 max_precession_angle = state.precession_angle.abs();
642 }
643 }
644 }
645
646 let dt = self.time_step;
648
649 let acc1 = self.calculate_acceleration(&position, &velocity, air_density, &wind_vector);
651
652 let pos2 = position + velocity * (dt * 0.5);
654 let vel2 = velocity + acc1 * (dt * 0.5);
655 let acc2 = self.calculate_acceleration(&pos2, &vel2, air_density, &wind_vector);
656
657 let pos3 = position + vel2 * (dt * 0.5);
659 let vel3 = velocity + acc2 * (dt * 0.5);
660 let acc3 = self.calculate_acceleration(&pos3, &vel3, air_density, &wind_vector);
661
662 let pos4 = position + vel3 * dt;
664 let vel4 = velocity + acc3 * dt;
665 let acc4 = self.calculate_acceleration(&pos4, &vel4, air_density, &wind_vector);
666
667 position += (velocity + vel2 * 2.0 + vel3 * 2.0 + vel4) * (dt / 6.0);
669 velocity += (acc1 + acc2 * 2.0 + acc3 * 2.0 + acc4) * (dt / 6.0);
670 time += dt;
671 }
672
673 let last_point = points.last().ok_or("No trajectory points generated")?;
675
676 let sampled_points = if self.inputs.enable_trajectory_sampling {
678 let trajectory_data = TrajectoryData {
679 times: points.iter().map(|p| p.time).collect(),
680 positions: points.iter().map(|p| p.position.clone()).collect(),
681 velocities: points.iter().map(|p| {
682 Vector3::new(0.0, 0.0, p.velocity_magnitude)
684 }).collect(),
685 transonic_distances: Vec::new(), };
687
688 let outputs = TrajectoryOutputs {
689 target_distance_horiz_m: last_point.position.z,
690 target_vertical_height_m: last_point.position.y,
691 time_of_flight_s: last_point.time,
692 max_ord_dist_horiz_m: max_height,
693 };
694
695 let samples = sample_trajectory(&trajectory_data, &outputs, self.inputs.sample_interval, self.inputs.mass);
697 Some(samples)
698 } else {
699 None
700 };
701
702 Ok(TrajectoryResult {
703 max_range: last_point.position.z,
704 max_height,
705 time_of_flight: last_point.time,
706 impact_velocity: last_point.velocity_magnitude,
707 impact_energy: last_point.kinetic_energy,
708 points,
709 sampled_points,
710 min_pitch_damping: if self.inputs.enable_pitch_damping { Some(min_pitch_damping) } else { None },
711 transonic_mach: transonic_mach,
712 angular_state: angular_state,
713 max_yaw_angle: if self.inputs.enable_precession_nutation { Some(max_yaw_angle) } else { None },
714 max_precession_angle: if self.inputs.enable_precession_nutation { Some(max_precession_angle) } else { None },
715 })
716 }
717
718 fn calculate_acceleration(&self, position: &Vector3<f64>, velocity: &Vector3<f64>, air_density: f64, wind_vector: &Vector3<f64>) -> Vector3<f64> {
719 let actual_wind = if self.inputs.enable_wind_shear {
721 self.get_wind_at_altitude(position.y)
722 } else {
723 wind_vector.clone()
724 };
725
726 let relative_velocity = velocity - &actual_wind;
727 let velocity_magnitude = relative_velocity.magnitude();
728
729 if velocity_magnitude < 0.001 {
730 return Vector3::new(0.0, -9.81, 0.0);
731 }
732
733 let cd = self.calculate_drag_coefficient(velocity_magnitude);
735 let reference_area = std::f64::consts::PI * (self.inputs.diameter / 2.0).powi(2);
736 let drag_magnitude = 0.5 * air_density * velocity_magnitude.powi(2) * cd * reference_area / self.inputs.ballistic_coefficient;
737
738 let drag_force = -relative_velocity.normalize() * drag_magnitude;
740
741 let acceleration = drag_force / self.inputs.mass + Vector3::new(0.0, -9.81, 0.0);
743
744 acceleration
745 }
746
747 fn calculate_drag_coefficient(&self, velocity: f64) -> f64 {
748 let temp_c = self.atmosphere.temperature;
751 let temp_k = temp_c + 273.15;
752 let gamma = 1.4; let r_specific = 287.05; let speed_of_sound = (gamma * r_specific * temp_k).sqrt();
755 let mach = velocity / speed_of_sound;
756
757 let base_cd = match self.inputs.drag_model {
759 DragModel::G1 => 0.5,
760 DragModel::G7 => 0.4,
761 _ => 0.45,
762 };
763
764 let projectile_shape = if let Some(ref model) = self.inputs.bullet_model {
766 if model.to_lowercase().contains("boat") || model.to_lowercase().contains("bt") {
768 ProjectileShape::BoatTail
769 } else if model.to_lowercase().contains("round") || model.to_lowercase().contains("rn") {
770 ProjectileShape::RoundNose
771 } else if model.to_lowercase().contains("flat") || model.to_lowercase().contains("fb") {
772 ProjectileShape::FlatBase
773 } else {
774 get_projectile_shape(
776 self.inputs.diameter,
777 self.inputs.mass / 0.00006479891, &self.inputs.drag_model.to_string()
779 )
780 }
781 } else {
782 get_projectile_shape(
784 self.inputs.diameter,
785 self.inputs.mass / 0.00006479891, &self.inputs.drag_model.to_string()
787 )
788 };
789
790 let include_wave_drag = self.inputs.enable_advanced_effects;
793 transonic_correction(mach, base_cd, projectile_shape, include_wave_drag)
794 }
795}
796
797#[derive(Debug, Clone)]
799pub struct MonteCarloParams {
800 pub num_simulations: usize,
801 pub velocity_std_dev: f64,
802 pub angle_std_dev: f64,
803 pub bc_std_dev: f64,
804 pub wind_speed_std_dev: f64,
805 pub target_distance: Option<f64>,
806 pub base_wind_speed: f64,
807 pub base_wind_direction: f64,
808 pub azimuth_std_dev: f64, }
810
811impl Default for MonteCarloParams {
812 fn default() -> Self {
813 Self {
814 num_simulations: 1000,
815 velocity_std_dev: 1.0,
816 angle_std_dev: 0.001,
817 bc_std_dev: 0.01,
818 wind_speed_std_dev: 1.0,
819 target_distance: None,
820 base_wind_speed: 0.0,
821 base_wind_direction: 0.0,
822 azimuth_std_dev: 0.001, }
824 }
825}
826
827#[derive(Debug, Clone)]
829pub struct MonteCarloResults {
830 pub ranges: Vec<f64>,
831 pub impact_velocities: Vec<f64>,
832 pub impact_positions: Vec<Vector3<f64>>,
833}
834
835pub fn run_monte_carlo(
837 base_inputs: BallisticInputs,
838 params: MonteCarloParams,
839) -> Result<MonteCarloResults, BallisticsError> {
840 let base_wind = WindConditions {
841 speed: params.base_wind_speed,
842 direction: params.base_wind_direction,
843 };
844 run_monte_carlo_with_wind(base_inputs, base_wind, params)
845}
846
847pub fn run_monte_carlo_with_wind(
849 base_inputs: BallisticInputs,
850 base_wind: WindConditions,
851 params: MonteCarloParams,
852) -> Result<MonteCarloResults, BallisticsError> {
853 use rand::{thread_rng, Rng};
854 use rand_distr::{Distribution, Normal};
855
856 let mut rng = thread_rng();
857 let mut ranges = Vec::new();
858 let mut impact_velocities = Vec::new();
859 let mut impact_positions = Vec::new();
860
861 let baseline_solver = TrajectorySolver::new(base_inputs.clone(), base_wind.clone(), Default::default());
863 let baseline_result = baseline_solver.solve()?;
864 let baseline_impact = baseline_result.points.last()
865 .ok_or("No baseline trajectory points")?
866 .position.clone();
867
868 let velocity_dist = Normal::new(base_inputs.muzzle_velocity, params.velocity_std_dev)
870 .map_err(|e| format!("Invalid velocity distribution: {}", e))?;
871 let angle_dist = Normal::new(base_inputs.launch_angle, params.angle_std_dev)
872 .map_err(|e| format!("Invalid angle distribution: {}", e))?;
873 let bc_dist = Normal::new(base_inputs.ballistic_coefficient, params.bc_std_dev)
874 .map_err(|e| format!("Invalid BC distribution: {}", e))?;
875 let wind_speed_dist = Normal::new(base_wind.speed, params.wind_speed_std_dev)
876 .map_err(|e| format!("Invalid wind speed distribution: {}", e))?;
877 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))?;
879 let azimuth_dist = Normal::new(base_inputs.azimuth_angle, params.azimuth_std_dev)
880 .map_err(|e| format!("Invalid azimuth distribution: {}", e))?;
881
882 let distance = baseline_impact.z; let pointing_error_dist = Normal::new(0.0, params.angle_std_dev * distance)
885 .map_err(|e| format!("Invalid pointing distribution: {}", e))?;
886
887 for _ in 0..params.num_simulations {
888 let mut inputs = base_inputs.clone();
890 inputs.muzzle_velocity = velocity_dist.sample(&mut rng).max(0.0);
891 inputs.launch_angle = angle_dist.sample(&mut rng);
892 inputs.ballistic_coefficient = bc_dist.sample(&mut rng).max(0.01);
893 inputs.azimuth_angle = azimuth_dist.sample(&mut rng); let wind = WindConditions {
897 speed: wind_speed_dist.sample(&mut rng).abs(),
898 direction: wind_dir_dist.sample(&mut rng),
899 };
900
901 let solver = TrajectorySolver::new(inputs, wind, Default::default());
903 match solver.solve() {
904 Ok(result) => {
905 ranges.push(result.max_range);
906 impact_velocities.push(result.impact_velocity);
907 if let Some(last_point) = result.points.last() {
908 let mut deviation = Vector3::new(
910 last_point.position.x - baseline_impact.x, last_point.position.y - baseline_impact.y, last_point.position.z - baseline_impact.z, );
914
915 let pointing_error_y = pointing_error_dist.sample(&mut rng);
918 deviation.y += pointing_error_y;
919
920 impact_positions.push(deviation);
921 }
922 },
923 Err(_) => {
924 continue;
926 }
927 }
928 }
929
930 if ranges.is_empty() {
931 return Err("No successful simulations".into());
932 }
933
934 Ok(MonteCarloResults {
935 ranges,
936 impact_velocities,
937 impact_positions,
938 })
939}
940
941pub fn calculate_zero_angle(
943 inputs: BallisticInputs,
944 target_distance: f64,
945 target_height: f64,
946) -> Result<f64, BallisticsError> {
947 calculate_zero_angle_with_conditions(
948 inputs,
949 target_distance,
950 target_height,
951 WindConditions::default(),
952 AtmosphericConditions::default(),
953 )
954}
955
956pub fn calculate_zero_angle_with_conditions(
957 inputs: BallisticInputs,
958 target_distance: f64,
959 target_height: f64,
960 wind: WindConditions,
961 atmosphere: AtmosphericConditions,
962) -> Result<f64, BallisticsError> {
963 let mut low_angle = -0.2; let mut high_angle = 0.2; let tolerance = 0.00001; let max_iterations = 50;
968
969 for iteration in 0..max_iterations {
970 let mid_angle = (low_angle + high_angle) / 2.0;
971
972 let mut test_inputs = inputs.clone();
973 test_inputs.launch_angle = mid_angle;
974
975 let mut solver = TrajectorySolver::new(test_inputs, wind.clone(), atmosphere.clone());
976 solver.set_max_range(target_distance * 2.0);
978 solver.set_time_step(0.001);
979 let result = solver.solve()?;
980
981 eprintln!(" Iteration {}: angle = {:.6} rad ({:.4} deg)",
982 iteration, mid_angle, mid_angle * 180.0 / std::f64::consts::PI);
983
984 let mut height_at_target = None;
986 for i in 0..result.points.len() {
987 if result.points[i].position.z >= target_distance {
988 if i > 0 {
989 let p1 = &result.points[i - 1];
991 let p2 = &result.points[i];
992 let t = (target_distance - p1.position.z) / (p2.position.z - p1.position.z);
993 height_at_target = Some(p1.position.y + t * (p2.position.y - p1.position.y));
994 } else {
995 height_at_target = Some(result.points[i].position.y);
996 }
997 break;
998 }
999 }
1000
1001 match height_at_target {
1002 Some(height) => {
1003 let error = height - target_height;
1004 eprintln!(" Height at target: {:.6} m, target: {:.6} m, error: {:.6} m",
1005 height, target_height, error);
1006 if error.abs() < 0.001 {
1007 eprintln!(" Converged!");
1008 return Ok(mid_angle);
1009 }
1010
1011 if error > 0.0 {
1012 high_angle = mid_angle;
1013 } else {
1014 low_angle = mid_angle;
1015 }
1016 },
1017 None => {
1018 low_angle = mid_angle;
1020 }
1021 }
1022
1023 if (high_angle - low_angle).abs() < tolerance {
1024 return Ok(mid_angle);
1025 }
1026 }
1027
1028 Err("Failed to find zero angle".into())
1029}
1030
1031pub fn estimate_bc_from_trajectory(
1033 velocity: f64,
1034 mass: f64,
1035 diameter: f64,
1036 points: &[(f64, f64)], ) -> Result<f64, BallisticsError> {
1038 let mut best_bc = 0.5;
1040 let mut best_error = f64::MAX;
1041
1042 for bc in (100..1000).step_by(10) {
1044 let bc_value = bc as f64 / 1000.0;
1045
1046 let inputs = BallisticInputs {
1047 muzzle_velocity: velocity,
1048 ballistic_coefficient: bc_value,
1049 mass,
1050 diameter,
1051 ..Default::default()
1052 };
1053
1054 let solver = TrajectorySolver::new(inputs, Default::default(), Default::default());
1055 let result = solver.solve()?;
1056
1057 let mut total_error = 0.0;
1059 for (target_dist, target_drop) in points {
1060 let mut calculated_drop = None;
1062 for i in 0..result.points.len() {
1063 if result.points[i].position.z >= *target_dist {
1064 if i > 0 {
1065 let p1 = &result.points[i - 1];
1067 let p2 = &result.points[i];
1068 let t = (target_dist - p1.position.z) / (p2.position.z - p1.position.z);
1069 calculated_drop = Some(-(p1.position.y + t * (p2.position.y - p1.position.y)));
1070 } else {
1071 calculated_drop = Some(-result.points[i].position.y);
1072 }
1073 break;
1074 }
1075 }
1076
1077 if let Some(drop) = calculated_drop {
1078 let error = (drop - target_drop).abs();
1079 total_error += error * error;
1080 }
1081 }
1082
1083 if total_error < best_error {
1084 best_error = total_error;
1085 best_bc = bc_value;
1086 }
1087 }
1088
1089 Ok(best_bc)
1090}
1091
1092fn calculate_air_density(atmosphere: &AtmosphericConditions) -> f64 {
1094 let r_specific = 287.058; let temperature_k = atmosphere.temperature + 273.15;
1098
1099 let pressure_pa = atmosphere.pressure * 100.0;
1101
1102 let density = pressure_pa / (r_specific * temperature_k);
1104
1105 let altitude_factor = (-atmosphere.altitude / 8000.0).exp();
1107
1108 density * altitude_factor
1109}
1110
1111use rand;
1113use rand_distr;