1use crate::DragModel;
3use nalgebra::Vector3;
4use std::error::Error;
5use std::fmt;
6
7#[derive(Debug)]
9pub struct BallisticsError {
10 message: String,
11}
12
13impl fmt::Display for BallisticsError {
14 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
15 write!(f, "{}", self.message)
16 }
17}
18
19impl Error for BallisticsError {}
20
21impl From<String> for BallisticsError {
22 fn from(msg: String) -> Self {
23 BallisticsError { message: msg }
24 }
25}
26
27impl From<&str> for BallisticsError {
28 fn from(msg: &str) -> Self {
29 BallisticsError { message: msg.to_string() }
30 }
31}
32
33#[derive(Debug, Clone)]
35pub struct BallisticInputs {
36 pub muzzle_velocity: f64, pub launch_angle: f64, pub ballistic_coefficient: f64,
40 pub mass: f64, pub diameter: f64, pub drag_model: DragModel,
43 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 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,
65 pub use_powder_sensitivity: bool,
66 pub powder_temp_sensitivity: f64,
67 pub powder_temp: f64, pub tipoff_yaw: f64, pub tipoff_decay_distance: f64, pub use_bc_segments: bool,
71 pub bc_segments: Option<Vec<(f64, f64)>>, pub bc_segments_data: Option<Vec<crate::BCSegmentData>>, pub use_enhanced_spin_drift: bool,
74 pub use_form_factor: bool,
75 pub enable_wind_shear: bool,
76 pub wind_shear_model: String,
77
78 pub bc_type_str: Option<String>,
80 pub bullet_model: Option<String>,
81 pub bullet_id: Option<String>,
82}
83
84impl Default for BallisticInputs {
85 fn default() -> Self {
86 let mass_kg = 0.01;
87 let diameter_m = 0.00762;
88 let bc = 0.5;
89 let launch_angle_rad = 0.0;
90 let drag_model = DragModel::G1;
91
92 Self {
93 muzzle_velocity: 800.0,
95 launch_angle: launch_angle_rad,
96 ballistic_coefficient: bc,
97 mass: mass_kg,
98 diameter: diameter_m,
99 drag_model,
100 sight_height: 0.05,
101
102 altitude: 0.0,
104 bc_type: drag_model,
105 bc_value: bc,
106 caliber_inches: diameter_m / 0.0254, weight_grains: mass_kg / 0.00006479891, bullet_diameter: diameter_m,
109 bullet_mass: mass_kg,
110 bullet_length: diameter_m * 4.0, muzzle_angle: launch_angle_rad,
112 target_distance: 100.0,
113 temperature: 15.0,
114 twist_rate: 12.0, is_twist_right: true,
116 shooting_angle: 0.0,
117 latitude: None,
118 ground_threshold: -10.0,
119
120 enable_advanced_effects: false,
122 use_powder_sensitivity: false,
123 powder_temp_sensitivity: 0.0,
124 powder_temp: 15.0,
125 tipoff_yaw: 0.0,
126 tipoff_decay_distance: 50.0,
127 use_bc_segments: false,
128 bc_segments: None,
129 bc_segments_data: None,
130 use_enhanced_spin_drift: false,
131 use_form_factor: false,
132 enable_wind_shear: false,
133 wind_shear_model: "none".to_string(),
134
135 bc_type_str: None,
137 bullet_model: None,
138 bullet_id: None,
139 }
140 }
141}
142
143#[derive(Debug, Clone)]
145pub struct WindConditions {
146 pub speed: f64, pub direction: f64, }
149
150impl Default for WindConditions {
151 fn default() -> Self {
152 Self {
153 speed: 0.0,
154 direction: 0.0,
155 }
156 }
157}
158
159#[derive(Debug, Clone)]
161pub struct AtmosphericConditions {
162 pub temperature: f64, pub pressure: f64, pub humidity: f64, pub altitude: f64, }
167
168impl Default for AtmosphericConditions {
169 fn default() -> Self {
170 Self {
171 temperature: 15.0,
172 pressure: 1013.25,
173 humidity: 50.0,
174 altitude: 0.0,
175 }
176 }
177}
178
179#[derive(Debug, Clone)]
181pub struct TrajectoryPoint {
182 pub time: f64,
183 pub position: Vector3<f64>,
184 pub velocity_magnitude: f64,
185 pub kinetic_energy: f64,
186}
187
188#[derive(Debug, Clone)]
190pub struct TrajectoryResult {
191 pub max_range: f64,
192 pub max_height: f64,
193 pub time_of_flight: f64,
194 pub impact_velocity: f64,
195 pub impact_energy: f64,
196 pub points: Vec<TrajectoryPoint>,
197}
198
199pub struct TrajectorySolver {
201 inputs: BallisticInputs,
202 wind: WindConditions,
203 atmosphere: AtmosphericConditions,
204 max_range: f64,
205 time_step: f64,
206}
207
208impl TrajectorySolver {
209 pub fn new(mut inputs: BallisticInputs, wind: WindConditions, atmosphere: AtmosphericConditions) -> Self {
210 inputs.bc_type = inputs.drag_model;
212 inputs.bc_value = inputs.ballistic_coefficient;
213 inputs.bullet_diameter = inputs.diameter;
214 inputs.bullet_mass = inputs.mass;
215 inputs.muzzle_angle = inputs.launch_angle;
216 inputs.caliber_inches = inputs.diameter / 0.0254;
217 inputs.weight_grains = inputs.mass / 0.00006479891;
218
219 Self {
220 inputs,
221 wind,
222 atmosphere,
223 max_range: 1000.0,
224 time_step: 0.001,
225 }
226 }
227
228 pub fn set_max_range(&mut self, range: f64) {
229 self.max_range = range;
230 }
231
232 pub fn set_time_step(&mut self, step: f64) {
233 self.time_step = step;
234 }
235
236 pub fn solve(&self) -> Result<TrajectoryResult, BallisticsError> {
237 let mut time = 0.0;
239 let mut position = Vector3::new(0.0, self.inputs.sight_height, 0.0);
240 let mut velocity = Vector3::new(
241 self.inputs.muzzle_velocity * self.inputs.launch_angle.cos(),
242 self.inputs.muzzle_velocity * self.inputs.launch_angle.sin(),
243 0.0,
244 );
245
246 let mut points = Vec::new();
247 let mut max_height = position.y;
248
249 let air_density = calculate_air_density(&self.atmosphere);
251
252 let wind_vector = Vector3::new(
254 self.wind.speed * self.wind.direction.sin(),
255 0.0,
256 self.wind.speed * self.wind.direction.cos(),
257 );
258
259 while position.x < self.max_range && position.y >= 0.0 && time < 100.0 {
261 let velocity_magnitude = velocity.magnitude();
263 let kinetic_energy = 0.5 * self.inputs.mass * velocity_magnitude * velocity_magnitude;
264
265 points.push(TrajectoryPoint {
266 time,
267 position: position.clone(),
268 velocity_magnitude,
269 kinetic_energy,
270 });
271
272 if position.y > max_height {
274 max_height = position.y;
275 }
276
277 let velocity_rel = velocity - wind_vector;
279 let velocity_rel_mag = velocity_rel.magnitude();
280 let drag_coefficient = self.calculate_drag_coefficient(velocity_rel_mag);
281
282 let drag_force = 0.5 * air_density * drag_coefficient *
284 self.inputs.diameter * self.inputs.diameter *
285 std::f64::consts::PI / 4.0 * velocity_rel_mag * velocity_rel_mag;
286
287 let drag_acceleration = -drag_force / self.inputs.mass;
289 let acceleration = Vector3::new(
290 drag_acceleration * velocity_rel.x / velocity_rel_mag,
291 drag_acceleration * velocity_rel.y / velocity_rel_mag - 9.80665,
292 drag_acceleration * velocity_rel.z / velocity_rel_mag,
293 );
294
295 velocity += acceleration * self.time_step;
297 position += velocity * self.time_step;
298 time += self.time_step;
299 }
300
301 let last_point = points.last().ok_or("No trajectory points generated")?;
303
304 Ok(TrajectoryResult {
305 max_range: last_point.position.x,
306 max_height,
307 time_of_flight: last_point.time,
308 impact_velocity: last_point.velocity_magnitude,
309 impact_energy: last_point.kinetic_energy,
310 points,
311 })
312 }
313
314 fn calculate_drag_coefficient(&self, velocity: f64) -> f64 {
315 let mach = velocity / 343.0; let base_cd = match self.inputs.drag_model {
321 DragModel::G1 => 0.5,
322 DragModel::G7 => 0.4,
323 _ => 0.45,
324 };
325
326 if mach > 0.8 && mach < 1.2 {
328 base_cd * 1.5
329 } else if mach > 1.2 {
330 base_cd * 0.8
331 } else {
332 base_cd
333 }
334 }
335}
336
337#[derive(Debug, Clone)]
339pub struct MonteCarloParams {
340 pub num_simulations: usize,
341 pub velocity_std_dev: f64,
342 pub angle_std_dev: f64,
343 pub bc_std_dev: f64,
344 pub wind_speed_std_dev: f64,
345 pub target_distance: Option<f64>,
346}
347
348impl Default for MonteCarloParams {
349 fn default() -> Self {
350 Self {
351 num_simulations: 1000,
352 velocity_std_dev: 1.0,
353 angle_std_dev: 0.001,
354 bc_std_dev: 0.01,
355 wind_speed_std_dev: 1.0,
356 target_distance: None,
357 }
358 }
359}
360
361#[derive(Debug, Clone)]
363pub struct MonteCarloResults {
364 pub ranges: Vec<f64>,
365 pub impact_velocities: Vec<f64>,
366 pub impact_positions: Vec<Vector3<f64>>,
367}
368
369pub fn run_monte_carlo(
371 base_inputs: BallisticInputs,
372 params: MonteCarloParams,
373) -> Result<MonteCarloResults, BallisticsError> {
374 use rand::{thread_rng, Rng};
375 use rand_distr::{Distribution, Normal};
376
377 let mut rng = thread_rng();
378 let mut ranges = Vec::new();
379 let mut impact_velocities = Vec::new();
380 let mut impact_positions = Vec::new();
381
382 let velocity_dist = Normal::new(base_inputs.muzzle_velocity, params.velocity_std_dev)
384 .map_err(|e| format!("Invalid velocity distribution: {}", e))?;
385 let angle_dist = Normal::new(base_inputs.launch_angle, params.angle_std_dev)
386 .map_err(|e| format!("Invalid angle distribution: {}", e))?;
387 let bc_dist = Normal::new(base_inputs.ballistic_coefficient, params.bc_std_dev)
388 .map_err(|e| format!("Invalid BC distribution: {}", e))?;
389 let wind_dist = Normal::new(0.0, params.wind_speed_std_dev)
390 .map_err(|e| format!("Invalid wind distribution: {}", e))?;
391
392 for _ in 0..params.num_simulations {
393 let mut inputs = base_inputs.clone();
395 inputs.muzzle_velocity = velocity_dist.sample(&mut rng).max(0.0);
396 inputs.launch_angle = angle_dist.sample(&mut rng);
397 inputs.ballistic_coefficient = bc_dist.sample(&mut rng).max(0.01);
398
399 let wind = WindConditions {
401 speed: wind_dist.sample(&mut rng).abs(),
402 direction: rng.gen_range(0.0..2.0 * std::f64::consts::PI),
403 };
404
405 let solver = TrajectorySolver::new(inputs, wind, Default::default());
407 match solver.solve() {
408 Ok(result) => {
409 ranges.push(result.max_range);
410 impact_velocities.push(result.impact_velocity);
411 if let Some(last_point) = result.points.last() {
412 impact_positions.push(last_point.position.clone());
413 }
414 },
415 Err(_) => {
416 continue;
418 }
419 }
420 }
421
422 if ranges.is_empty() {
423 return Err("No successful simulations".into());
424 }
425
426 Ok(MonteCarloResults {
427 ranges,
428 impact_velocities,
429 impact_positions,
430 })
431}
432
433pub fn calculate_zero_angle(
435 inputs: BallisticInputs,
436 target_distance: f64,
437 target_height: f64,
438) -> Result<f64, BallisticsError> {
439 calculate_zero_angle_with_conditions(
440 inputs,
441 target_distance,
442 target_height,
443 WindConditions::default(),
444 AtmosphericConditions::default(),
445 )
446}
447
448pub fn calculate_zero_angle_with_conditions(
449 inputs: BallisticInputs,
450 target_distance: f64,
451 target_height: f64,
452 wind: WindConditions,
453 atmosphere: AtmosphericConditions,
454) -> Result<f64, BallisticsError> {
455 let mut low_angle = -0.2; let mut high_angle = 0.2; let tolerance = 0.00001; let max_iterations = 50;
460
461 for iteration in 0..max_iterations {
462 let mid_angle = (low_angle + high_angle) / 2.0;
463
464 let mut test_inputs = inputs.clone();
465 test_inputs.launch_angle = mid_angle;
466
467 let mut solver = TrajectorySolver::new(test_inputs, wind.clone(), atmosphere.clone());
468 solver.set_max_range(target_distance * 2.0);
470 solver.set_time_step(0.001);
471 let result = solver.solve()?;
472
473 eprintln!(" Iteration {}: angle = {:.6} rad ({:.4} deg)",
474 iteration, mid_angle, mid_angle * 180.0 / std::f64::consts::PI);
475
476 let mut height_at_target = None;
478 for i in 0..result.points.len() {
479 if result.points[i].position.x >= target_distance {
480 if i > 0 {
481 let p1 = &result.points[i - 1];
483 let p2 = &result.points[i];
484 let t = (target_distance - p1.position.x) / (p2.position.x - p1.position.x);
485 height_at_target = Some(p1.position.y + t * (p2.position.y - p1.position.y));
486 } else {
487 height_at_target = Some(result.points[i].position.y);
488 }
489 break;
490 }
491 }
492
493 match height_at_target {
494 Some(height) => {
495 let error = height - target_height;
496 eprintln!(" Height at target: {:.6} m, target: {:.6} m, error: {:.6} m",
497 height, target_height, error);
498 if error.abs() < 0.001 {
499 eprintln!(" Converged!");
500 return Ok(mid_angle);
501 }
502
503 if error > 0.0 {
504 high_angle = mid_angle;
505 } else {
506 low_angle = mid_angle;
507 }
508 },
509 None => {
510 low_angle = mid_angle;
512 }
513 }
514
515 if (high_angle - low_angle).abs() < tolerance {
516 return Ok(mid_angle);
517 }
518 }
519
520 Err("Failed to find zero angle".into())
521}
522
523pub fn estimate_bc_from_trajectory(
525 velocity: f64,
526 mass: f64,
527 diameter: f64,
528 points: &[(f64, f64)], ) -> Result<f64, BallisticsError> {
530 let mut best_bc = 0.5;
532 let mut best_error = f64::MAX;
533
534 for bc in (100..1000).step_by(10) {
536 let bc_value = bc as f64 / 1000.0;
537
538 let inputs = BallisticInputs {
539 muzzle_velocity: velocity,
540 ballistic_coefficient: bc_value,
541 mass,
542 diameter,
543 ..Default::default()
544 };
545
546 let solver = TrajectorySolver::new(inputs, Default::default(), Default::default());
547 let result = solver.solve()?;
548
549 let mut total_error = 0.0;
551 for (target_dist, target_drop) in points {
552 let mut calculated_drop = None;
554 for i in 0..result.points.len() {
555 if result.points[i].position.x >= *target_dist {
556 if i > 0 {
557 let p1 = &result.points[i - 1];
559 let p2 = &result.points[i];
560 let t = (target_dist - p1.position.x) / (p2.position.x - p1.position.x);
561 calculated_drop = Some(-(p1.position.y + t * (p2.position.y - p1.position.y)));
562 } else {
563 calculated_drop = Some(-result.points[i].position.y);
564 }
565 break;
566 }
567 }
568
569 if let Some(drop) = calculated_drop {
570 let error = (drop - target_drop).abs();
571 total_error += error * error;
572 }
573 }
574
575 if total_error < best_error {
576 best_error = total_error;
577 best_bc = bc_value;
578 }
579 }
580
581 Ok(best_bc)
582}
583
584fn calculate_air_density(atmosphere: &AtmosphericConditions) -> f64 {
586 let r_specific = 287.058; let temperature_k = atmosphere.temperature + 273.15;
590
591 let pressure_pa = atmosphere.pressure * 100.0;
593
594 let density = pressure_pa / (r_specific * temperature_k);
596
597 let altitude_factor = (-atmosphere.altitude / 8000.0).exp();
599
600 density * altitude_factor
601}
602
603use rand;
605use rand_distr;