1use crate::atmosphere::{get_direct_atmosphere, get_local_atmosphere};
2use crate::bc_estimation::BCSegmentEstimator;
3use crate::constants::*;
4use crate::drag::get_drag_coefficient_full;
5use crate::form_factor::apply_form_factor_to_drag;
6use crate::spin_drift::{apply_enhanced_spin_drift, calculate_enhanced_spin_drift};
7use crate::InternalBallisticInputs as BallisticInputs;
8use nalgebra::Vector3;
9
10const INCHES_PER_FOOT: f64 = 12.0;
12const INCHES_TO_METERS: f64 = 0.0254;
13const STANDARD_AIR_DENSITY_METRIC: f64 = 1.225; const MAGNUS_COEFF_SUBSONIC: f64 = 0.030;
28
29const MAGNUS_COEFF_TRANSONIC_REDUCTION: f64 = 0.0075;
36
37const MAGNUS_COEFF_SUPERSONIC_BASE: f64 = 0.015;
43
44const MAGNUS_COEFF_SUPERSONIC_SCALE: f64 = 0.0044;
50
51const MAGNUS_TRANSONIC_LOWER: f64 = 0.8; const MAGNUS_TRANSONIC_UPPER: f64 = 1.2; const MAGNUS_TRANSONIC_RANGE: f64 = 0.4; const MAGNUS_SUPERSONIC_RANGE: f64 = 1.8; const MAX_REALISTIC_DENSITY: f64 = 2.0; const MIN_REALISTIC_SPEED_OF_SOUND: f64 = 200.0; fn calculate_spin_rate(twist_rate: f64, velocity_mps: f64) -> f64 {
67 if twist_rate <= 0.0 {
68 return 0.0;
69 }
70
71 let velocity_fps = velocity_mps * MPS_TO_FPS;
73 let twist_rate_ft = twist_rate / INCHES_PER_FOOT;
74
75 let revolutions_per_second = velocity_fps / twist_rate_ft;
78
79 revolutions_per_second * 2.0 * std::f64::consts::PI
80}
81
82fn calculate_magnus_moment_coefficient(mach: f64) -> f64 {
85 if mach < MAGNUS_TRANSONIC_LOWER {
89 MAGNUS_COEFF_SUBSONIC
91 } else if mach < MAGNUS_TRANSONIC_UPPER {
92 MAGNUS_COEFF_SUBSONIC
95 - MAGNUS_COEFF_TRANSONIC_REDUCTION * (mach - MAGNUS_TRANSONIC_LOWER)
96 / MAGNUS_TRANSONIC_RANGE
97 } else {
98 MAGNUS_COEFF_SUPERSONIC_BASE
100 + MAGNUS_COEFF_SUPERSONIC_SCALE
101 * ((mach - MAGNUS_TRANSONIC_UPPER) / MAGNUS_SUPERSONIC_RANGE).min(1.0)
102 }
103}
104
105pub fn compute_derivatives(
107 pos: Vector3<f64>,
108 vel: Vector3<f64>,
109 inputs: &BallisticInputs,
110 wind_vector: Vector3<f64>,
111 atmos_params: (f64, f64, f64, f64),
112 bc_used: f64,
113 omega_vector: Option<Vector3<f64>>,
114 time: f64,
115) -> [f64; 6] {
116 let accel_gravity = Vector3::new(0.0, -G_ACCEL_MPS2, 0.0);
118
119 let velocity_adjusted = vel - wind_vector;
121 let speed_air = velocity_adjusted.norm();
122
123 let mut accel_drag = Vector3::zeros();
125 let mut accel_magnus = Vector3::zeros();
126
127 if speed_air > crate::constants::MIN_VELOCITY_THRESHOLD {
129 let v_rel_fps = speed_air * MPS_TO_FPS;
130
131 let altitude_at_pos = inputs.altitude + pos[1];
133
134 let (air_density, speed_of_sound) = if atmos_params.0 < MAX_REALISTIC_DENSITY
140 && atmos_params.1 > MIN_REALISTIC_SPEED_OF_SOUND
141 && atmos_params.2 == 0.0
142 && atmos_params.3 == 0.0
143 {
144 get_direct_atmosphere(atmos_params.0, atmos_params.1)
146 } else {
147 get_local_atmosphere(
149 altitude_at_pos,
150 atmos_params.0, atmos_params.1, atmos_params.2, atmos_params.3, )
155 };
156
157 let mach = if speed_of_sound > 1e-9 {
159 speed_air / speed_of_sound
160 } else {
161 0.0 };
163
164 let mut drag_factor = get_drag_coefficient_full(
166 mach,
167 &inputs.bc_type,
168 true, true, None, if inputs.caliber_inches > 0.0 {
172 Some(inputs.caliber_inches)
173 } else {
174 Some(inputs.bullet_diameter)
175 },
176 if inputs.weight_grains > 0.0 {
177 Some(inputs.weight_grains)
178 } else {
179 Some(inputs.bullet_mass * 15.432358)
180 },
181 Some(speed_air),
182 Some(air_density),
183 Some(atmos_params.1), );
185
186 if inputs.use_form_factor {
188 drag_factor = apply_form_factor_to_drag(
189 drag_factor,
190 inputs.bullet_model.as_deref(),
191 &inputs.bc_type,
192 true,
193 );
194 }
195
196 let mut bc_val = bc_used;
198
199 if inputs.use_bc_segments {
200 if inputs.bc_segments_data.is_some() {
202 bc_val = get_bc_for_velocity(v_rel_fps, inputs, bc_used);
203 } else if let Some(ref segments) = inputs.bc_segments {
204 bc_val = interpolated_bc(mach, segments, Some(inputs));
206 } else {
207 bc_val = get_bc_for_velocity(v_rel_fps, inputs, bc_used);
209 }
210 } else if let Some(ref segments) = inputs.bc_segments {
211 bc_val = interpolated_bc(mach, segments, Some(inputs));
213 }
214
215 let yaw_deg = if inputs.tipoff_decay_distance.abs() > 1e-9 {
217 inputs.tipoff_yaw * (-pos[0] / inputs.tipoff_decay_distance).exp()
218 } else {
219 inputs.tipoff_yaw };
221 let yaw_rad = yaw_deg.to_radians();
222 let yaw_multiplier = 1.0 + yaw_rad.powi(2);
223
224 let density_scale = air_density / STANDARD_AIR_DENSITY;
226
227 let drag_factor = if mach > 0.7 && mach < 1.3 {
229 let shape = crate::transonic_drag::get_projectile_shape(
231 inputs.bullet_diameter,
232 inputs.bullet_mass,
233 &inputs.bc_type.to_string(),
234 );
235
236 let corrected_cd = crate::transonic_drag::transonic_correction(
238 mach,
239 drag_factor,
240 shape,
241 true, );
243
244 corrected_cd / drag_factor
246 } else {
247 1.0
248 } * drag_factor;
249
250 let drag_factor = if mach < 1.0 && speed_air < 200.0 {
252 let temperature_c = atmos_params.1; crate::reynolds::apply_reynolds_correction(
258 drag_factor,
259 speed_air,
260 inputs.bullet_diameter,
261 air_density,
262 temperature_c,
263 mach,
264 )
265 } else {
266 drag_factor
267 };
268
269 let standard_factor = drag_factor * CD_TO_RETARD;
271 let a_drag_ft_s2 =
272 (v_rel_fps.powi(2) * standard_factor * yaw_multiplier * density_scale) / bc_val;
273 let a_drag_m_s2 = a_drag_ft_s2 * FPS_TO_MPS;
274
275 accel_drag = -a_drag_m_s2 * (velocity_adjusted / speed_air);
277
278 if inputs.enable_advanced_effects && inputs.bullet_diameter > 0.0 && inputs.twist_rate > 0.0
280 {
281 let spin_rate_rad_s = calculate_spin_rate(inputs.twist_rate, speed_air);
283
284 let c_la = calculate_magnus_moment_coefficient(mach);
286
287 let diameter_m = inputs.bullet_diameter * INCHES_TO_METERS;
289
290 let spin_param = if speed_air > 1e-9 {
297 spin_rate_rad_s * diameter_m / (2.0 * speed_air)
298 } else {
299 0.0 };
301
302 let c_l = spin_param * c_la;
304
305 let area = std::f64::consts::PI * (diameter_m / 2.0).powi(2);
307
308 const MAGNUS_CALIBRATION_FACTOR: f64 = 1.8; let magnus_force_magnitude =
315 MAGNUS_CALIBRATION_FACTOR * 0.5 * air_density * speed_air.powi(2) * area * c_l;
316
317 let velocity_unit = velocity_adjusted / speed_air;
320
321 let vertical = Vector3::new(0.0, 1.0, 0.0); let magnus_direction = velocity_unit.cross(&vertical);
331 let magnus_norm = magnus_direction.norm();
332
333 if magnus_norm > 1e-12 && magnus_force_magnitude > 1e-12 {
334 let magnus_direction = magnus_direction / magnus_norm;
335
336 let magnus_direction = if inputs.is_twist_right {
338 magnus_direction
339 } else {
340 -magnus_direction
341 };
342
343 let bullet_mass_kg = inputs.bullet_mass * GRAINS_TO_KG;
345
346 accel_magnus = (magnus_force_magnitude / bullet_mass_kg) * magnus_direction;
348 }
349 }
350 }
351
352 let mut accel = accel_gravity + accel_drag + accel_magnus;
354
355 if let Some(omega) = omega_vector {
357 let accel_coriolis = -2.0 * omega.cross(&vel);
358 accel += accel_coriolis;
359 }
360
361 let mut derivatives = [vel[0], vel[1], vel[2], accel[0], accel[1], accel[2]];
363
364 if inputs.use_enhanced_spin_drift && inputs.enable_advanced_effects && time > 0.0 {
365 let velocity_adjusted = vel - wind_vector;
367 let crosswind_speed = if velocity_adjusted.norm() > crate::constants::MIN_VELOCITY_THRESHOLD
368 {
369 let trajectory_unit = velocity_adjusted / velocity_adjusted.norm();
370 let crosswind = wind_vector - wind_vector.dot(&trajectory_unit) * trajectory_unit;
371 crosswind.norm()
372 } else {
373 0.0
374 };
375
376 let air_density = if speed_air > crate::constants::MIN_VELOCITY_THRESHOLD {
378 let altitude_at_pos = inputs.altitude + pos[1];
379 let (density, _) = if atmos_params.0 < MAX_REALISTIC_DENSITY
380 && atmos_params.1 > MIN_REALISTIC_SPEED_OF_SOUND
381 && atmos_params.2 == 0.0
382 && atmos_params.3 == 0.0
383 {
384 get_direct_atmosphere(atmos_params.0, atmos_params.1)
385 } else {
386 get_local_atmosphere(
387 altitude_at_pos,
388 atmos_params.0,
389 atmos_params.1,
390 atmos_params.2,
391 atmos_params.3,
392 )
393 };
394 density
395 } else {
396 STANDARD_AIR_DENSITY_METRIC };
398
399 let spin_components = calculate_enhanced_spin_drift(
401 inputs.bullet_mass,
402 vel.norm(),
403 inputs.twist_rate,
404 inputs.bullet_diameter,
405 inputs.bullet_length,
406 inputs.is_twist_right,
407 time,
408 air_density,
409 crosswind_speed,
410 0.0, false, );
413
414 apply_enhanced_spin_drift(
416 &mut derivatives,
417 &spin_components,
418 time,
419 inputs.is_twist_right,
420 );
421 }
422
423 derivatives
425}
426
427fn calculate_bc_fallback(
429 bullet_mass: Option<f64>, bullet_diameter: Option<f64>, bc_type: Option<&str>, ) -> f64 {
433 use crate::constants::*;
434
435 if let Some(weight) = bullet_mass {
437 let base_bc = if weight < 50.0 {
438 BC_FALLBACK_ULTRA_LIGHT
439 } else if weight < 100.0 {
440 BC_FALLBACK_LIGHT
441 } else if weight < 150.0 {
442 BC_FALLBACK_MEDIUM
443 } else if weight < 200.0 {
444 BC_FALLBACK_HEAVY
445 } else {
446 BC_FALLBACK_VERY_HEAVY
447 };
448
449 return if let Some(drag_model) = bc_type {
451 if drag_model == "G7" {
452 base_bc * 0.85 } else {
454 base_bc
455 }
456 } else {
457 base_bc
458 };
459 }
460
461 if let Some(caliber) = bullet_diameter {
463 let base_bc = if caliber <= 0.224 {
464 BC_FALLBACK_SMALL_CALIBER
465 } else if caliber <= 0.243 {
466 BC_FALLBACK_MEDIUM_CALIBER
467 } else if caliber <= 0.284 {
468 BC_FALLBACK_LARGE_CALIBER
469 } else {
470 BC_FALLBACK_XLARGE_CALIBER
471 };
472
473 return if let Some(drag_model) = bc_type {
475 if drag_model == "G7" {
476 base_bc * 0.85 } else {
478 base_bc
479 }
480 } else {
481 base_bc
482 };
483 }
484
485 let base_fallback = BC_FALLBACK_CONSERVATIVE;
487 if let Some(drag_model) = bc_type {
488 if drag_model == "G7" {
489 return base_fallback * 0.85;
490 }
491 }
492
493 base_fallback
494}
495
496pub fn interpolated_bc(
498 mach: f64,
499 segments: &[(f64, f64)],
500 inputs: Option<&BallisticInputs>,
501) -> f64 {
502 if segments.is_empty() {
503 if let Some(inputs) = inputs {
505 let bc_type_str = match inputs.bc_type {
506 crate::DragModel::G1 => "G1",
507 crate::DragModel::G7 => "G7",
508 _ => "G1", };
510 return calculate_bc_fallback(
511 Some(inputs.bullet_mass),
512 Some(inputs.bullet_diameter),
513 Some(bc_type_str),
514 );
515 }
516 return crate::constants::BC_FALLBACK_CONSERVATIVE; }
518
519 if segments.len() == 1 {
520 return segments[0].1;
521 }
522
523 let mut sorted_segments = segments.to_vec();
525 sorted_segments.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
526
527 if mach <= sorted_segments[0].0 {
529 return sorted_segments[0].1;
530 }
531 if mach >= sorted_segments[sorted_segments.len() - 1].0 {
532 return sorted_segments[sorted_segments.len() - 1].1;
533 }
534
535 let idx = sorted_segments.partition_point(|(m, _)| *m <= mach);
537 if idx == 0 || idx >= sorted_segments.len() {
538 return sorted_segments[0].1;
540 }
541
542 let (mach1, bc1) = sorted_segments[idx - 1];
543 let (mach2, bc2) = sorted_segments[idx];
544
545 let denominator = mach2 - mach1;
547 if denominator.abs() < crate::constants::MIN_DIVISION_THRESHOLD {
548 return bc1; }
550 let t = (mach - mach1) / denominator;
551 bc1 + t * (bc2 - bc1)
552}
553
554fn get_bc_for_velocity(velocity_fps: f64, inputs: &BallisticInputs, bc_used: f64) -> f64 {
556 if !inputs.use_bc_segments {
558 return bc_used;
559 }
560
561 if let Some(ref bc_segments_data) = inputs.bc_segments_data {
563 for segment in bc_segments_data {
564 if velocity_fps >= segment.velocity_min && velocity_fps <= segment.velocity_max {
565 return segment.bc_value;
566 }
567 }
568 }
569
570 if inputs.bullet_diameter > 0.0 && inputs.bullet_mass > 0.0 && bc_used > 0.0 {
572 let model = if let Some(ref bullet_id) = inputs.bullet_id {
574 bullet_id.clone()
575 } else {
576 format!("{}gr bullet", inputs.bullet_mass as i32)
577 };
578
579 let bc_type_str = inputs.bc_type_str.as_deref().unwrap_or("G1");
581 let segments = BCSegmentEstimator::estimate_bc_segments(
582 bc_used,
583 inputs.caliber_inches,
584 inputs.weight_grains,
585 &model,
586 bc_type_str,
587 );
588
589 for segment in &segments {
591 if velocity_fps >= segment.velocity_min && velocity_fps <= segment.velocity_max {
592 return segment.bc_value;
593 }
594 }
595 }
596
597 bc_used
599}
600
601#[cfg(test)]
602mod tests {
603 use super::*;
604
605 fn create_test_inputs() -> BallisticInputs {
606 BallisticInputs {
607 muzzle_velocity: 800.0,
608 bc_value: 0.5,
609 bullet_mass: 0.0109, bullet_diameter: 0.00782, altitude: 1000.0,
612 ..Default::default()
613 }
614 }
615
616 #[test]
617 fn test_compute_derivatives_basic() {
618 let pos = Vector3::new(0.0, 0.0, 0.0);
619 let vel = Vector3::new(800.0, 0.0, 0.0);
620 let inputs = create_test_inputs();
621 let wind_vector = Vector3::zeros();
622 let atmos_params = (1.225, 340.0, 0.0, 0.0); let bc_used = 0.5;
625
626 let result = compute_derivatives(
627 pos,
628 vel,
629 &inputs,
630 wind_vector,
631 atmos_params,
632 bc_used,
633 None,
634 0.0,
635 );
636
637 assert_eq!(result.len(), 6);
639
640 assert!((result[0] - vel[0]).abs() < 1e-10);
642 assert!((result[1] - vel[1]).abs() < 1e-10);
643 assert!((result[2] - vel[2]).abs() < 1e-10);
644
645 assert!(result[4] < 0.0); assert!(result[3] < 0.0); }
651
652 #[test]
653 fn test_compute_derivatives_with_wind() {
654 let pos = Vector3::new(0.0, 0.0, 0.0);
655 let vel = Vector3::new(800.0, 0.0, 0.0);
656 let inputs = create_test_inputs();
657 let wind_vector = Vector3::new(10.0, 0.0, 0.0); let atmos_params = (1.225, 340.0, 0.0, 0.0); let bc_used = 0.5;
660
661 let result = compute_derivatives(
662 pos,
663 vel,
664 &inputs,
665 wind_vector,
666 atmos_params,
667 bc_used,
668 None,
669 0.0,
670 );
671
672 assert!(result[3] < 0.0); }
676
677 #[test]
678 fn test_compute_derivatives_with_coriolis() {
679 let pos = Vector3::new(0.0, 0.0, 0.0);
680 let vel = Vector3::new(800.0, 0.0, 0.0);
681 let inputs = create_test_inputs();
682 let wind_vector = Vector3::zeros();
683 let atmos_params = (1.225, 340.0, 0.0, 0.0); let bc_used = 0.5;
685 let omega = Vector3::new(0.0, 0.0, 7.2921e-5); let result = compute_derivatives(
688 pos,
689 vel,
690 &inputs,
691 wind_vector,
692 atmos_params,
693 bc_used,
694 Some(omega),
695 0.0,
696 );
697
698 assert!(result[4].abs() > 1e-3); }
701
702 #[test]
703 fn test_interpolated_bc() {
704 let segments = vec![(0.5, 0.4), (1.0, 0.5), (1.5, 0.6), (2.0, 0.5)];
705
706 assert!((interpolated_bc(1.0, &segments, None) - 0.5).abs() < 1e-10);
708
709 let bc_075 = interpolated_bc(0.75, &segments, None);
711 assert!(bc_075 > 0.4 && bc_075 < 0.5);
712
713 assert!((interpolated_bc(0.1, &segments, None) - 0.4).abs() < 1e-10);
715 assert!((interpolated_bc(3.0, &segments, None) - 0.5).abs() < 1e-10);
716 }
717
718 #[test]
719 fn test_interpolated_bc_edge_cases() {
720 assert!(
722 (interpolated_bc(1.0, &[], None) - crate::constants::BC_FALLBACK_CONSERVATIVE).abs()
723 < 1e-10
724 );
725
726 let single = vec![(1.0, 0.7)];
728 assert!((interpolated_bc(1.5, &single, None) - 0.7).abs() < 1e-10);
729 }
730
731 #[test]
732 fn test_magnus_effect() {
733 let pos = Vector3::new(0.0, 0.0, 0.0);
734 let vel = Vector3::new(822.96, 0.0, 0.0); let mut inputs = create_test_inputs();
736 inputs.bullet_diameter = 0.308; inputs.twist_rate = 10.0; inputs.is_twist_right = true;
739 inputs.enable_advanced_effects = true; let wind_vector = Vector3::zeros();
742 let atmos_params = (1.225, 340.0, 0.0, 0.0); let bc_used = 0.5;
744
745 let result = compute_derivatives(
746 pos,
747 vel,
748 &inputs,
749 wind_vector,
750 atmos_params,
751 bc_used,
752 None,
753 0.0,
754 );
755
756 assert!(result[5].abs() > 0.01); assert!(result[5] > 0.0); }
763
764 #[test]
765 fn test_magnus_moment_coefficient() {
766 assert!((calculate_magnus_moment_coefficient(0.5) - 0.030).abs() < 0.001); assert!((calculate_magnus_moment_coefficient(0.8) - 0.030).abs() < 0.001); assert!((calculate_magnus_moment_coefficient(1.0) - 0.02625).abs() < 0.001); assert!((calculate_magnus_moment_coefficient(1.2) - 0.015).abs() < 0.001); assert!((calculate_magnus_moment_coefficient(2.0) - 0.01653).abs() < 0.001);
772 }
774}