1use std::f64::consts::PI;
8
9#[derive(Debug, Clone)]
11pub struct SpinDriftCoefficients {
12 pub litz_coefficient: f64,
14 pub mccoy_jump_factor: f64,
16 pub transonic_factor: f64,
18 pub yaw_damping: f64,
20}
21
22impl SpinDriftCoefficients {
23 pub fn for_bullet_type(bullet_type: &str) -> Self {
25 match bullet_type.to_lowercase().as_str() {
26 "match" | "bthp" | "boat_tail" => Self {
27 litz_coefficient: 1.25,
28 mccoy_jump_factor: 0.85,
29 transonic_factor: 0.75,
30 yaw_damping: 0.92,
31 },
32 "vld" | "very_low_drag" => Self {
33 litz_coefficient: 1.15,
34 mccoy_jump_factor: 0.78,
35 transonic_factor: 0.68,
36 yaw_damping: 0.88,
37 },
38 "hybrid" | "hybrid_ogive" => Self {
39 litz_coefficient: 1.20,
40 mccoy_jump_factor: 0.82,
41 transonic_factor: 0.72,
42 yaw_damping: 0.90,
43 },
44 "flat_base" | "fb" => Self {
45 litz_coefficient: 1.35,
46 mccoy_jump_factor: 0.95,
47 transonic_factor: 0.85,
48 yaw_damping: 0.95,
49 },
50 _ => Self::default(),
51 }
52 }
53
54 pub fn default() -> Self {
55 Self {
56 litz_coefficient: 1.25,
57 mccoy_jump_factor: 0.85,
58 transonic_factor: 0.75,
59 yaw_damping: 0.92,
60 }
61 }
62}
63
64pub fn calculate_advanced_spin_drift(
66 stability_factor: f64,
67 time_of_flight_s: f64,
68 velocity_mps: f64,
69 muzzle_velocity_mps: f64,
70 spin_rate_rad_s: f64,
71 caliber_m: f64,
72 mass_kg: f64,
73 air_density_kg_m3: f64,
74 is_right_twist: bool,
75 bullet_type: &str,
76) -> f64 {
77 if time_of_flight_s <= 0.0
80 || stability_factor <= 0.0
81 || muzzle_velocity_mps <= 0.0
82 || air_density_kg_m3 <= 0.0
83 {
84 return 0.0;
85 }
86
87 let coeffs = SpinDriftCoefficients::for_bullet_type(bullet_type);
88
89 let sign = if is_right_twist { 1.0 } else { -1.0 };
91
92 let mach_current = velocity_mps / 343.0;
94 let mach_muzzle = muzzle_velocity_mps / 343.0;
95
96 let litz_drift =
98 calculate_litz_drift(stability_factor, time_of_flight_s, coeffs.litz_coefficient);
99
100 let jump_correction = calculate_aerodynamic_jump_correction(
102 mach_muzzle,
103 spin_rate_rad_s,
104 caliber_m,
105 mass_kg,
106 coeffs.mccoy_jump_factor,
107 );
108
109 let transonic_correction =
111 calculate_transonic_correction(mach_current, coeffs.transonic_factor);
112
113 let yaw_factor =
115 calculate_yaw_damping_factor(stability_factor, time_of_flight_s, coeffs.yaw_damping);
116
117 let velocity_ratio = velocity_mps / muzzle_velocity_mps;
119 let velocity_correction = velocity_ratio.powf(0.3);
120
121 let total_drift = sign
123 * (litz_drift * transonic_correction * yaw_factor * velocity_correction + jump_correction);
124
125 let density_correction = (1.225 / air_density_kg_m3).sqrt();
127
128 total_drift * density_correction
129}
130
131fn calculate_litz_drift(stability: f64, time_s: f64, coefficient: f64) -> f64 {
133 if stability <= 1.0 || time_s <= 0.0 {
134 return 0.0;
135 }
136
137 let base_drift = coefficient * (stability + 1.2) * time_s.powf(1.83);
141
142 base_drift * 0.0254
144}
145
146fn calculate_aerodynamic_jump_correction(
148 mach: f64,
149 spin_rate_rad_s: f64,
150 caliber_m: f64,
151 mass_kg: f64,
152 jump_factor: f64,
153) -> f64 {
154 if mach <= 0.0 {
159 return 0.0;
160 }
161
162 let spin_parameter = spin_rate_rad_s * caliber_m / (2.0 * 343.0 * mach);
163
164 let jump_mrad = jump_factor * spin_parameter * (mass_kg / 0.01).sqrt();
166
167 jump_mrad * 0.001 * 100.0 }
171
172fn calculate_transonic_correction(mach: f64, transonic_factor: f64) -> f64 {
174 if mach < 0.8 {
175 1.0
177 } else if mach < 1.2 {
178 let transonic_ratio = (mach - 0.8) / 0.4;
181
182 1.0 + (transonic_factor - 1.0) * (1.0 - (transonic_ratio * PI).cos()) / 2.0
183 } else {
184 0.85 + 0.15 * (2.5 - mach).max(0.0) / 1.3
186 }
187}
188
189fn calculate_yaw_damping_factor(stability: f64, time_s: f64, damping_coeff: f64) -> f64 {
191 let damping_rate = damping_coeff * stability.sqrt();
194 let damped = 1.0 - (-damping_rate * time_s).exp();
195
196 damped.max(0.5).min(1.0)
198}
199
200pub fn calculate_advanced_yaw_of_repose(
202 stability_factor: f64,
203 velocity_mps: f64,
204 crosswind_mps: f64,
205 spin_rate_rad_s: f64,
206 air_density_kg_m3: f64,
207 caliber_m: f64,
208) -> f64 {
209 if stability_factor <= 1.0 || velocity_mps <= 0.0 {
210 return 0.0;
211 }
212
213 let wind_yaw = if crosswind_mps != 0.0 && velocity_mps > 0.0 {
215 (crosswind_mps / velocity_mps).atan()
216 } else {
217 0.001 + 0.0005 * (velocity_mps / 800.0).min(2.0)
220 };
221
222 let stability_term = ((stability_factor - 1.0) / stability_factor).sqrt();
224
225 let q = 0.5 * air_density_kg_m3 * velocity_mps.powi(2);
227 let q_factor = (q / 50000.0).min(1.5).max(0.5); let spin_factor = if spin_rate_rad_s > 0.0 {
231 let spin_param = spin_rate_rad_s * caliber_m / (2.0 * velocity_mps);
232 1.0 + 0.2 * spin_param.min(0.5)
233 } else {
234 1.0
235 };
236
237 wind_yaw * stability_term * q_factor * spin_factor
238}
239
240pub fn apply_ml_correction(
242 base_drift: f64,
243 stability: f64,
244 mach: f64,
245 time_s: f64,
246 caliber_inches: f64,
247 mass_grains: f64,
248) -> f64 {
249 let mut correction = 1.0;
260
261 if stability > 2.5 && mach < 1.0 {
263 correction *= 0.92; }
265
266 if time_s > 2.0 && mach < 0.9 {
267 correction *= 1.08; }
269
270 if caliber_inches < 0.264 && mass_grains < 100.0 {
271 correction *= 0.88; }
273
274 base_drift * correction
275}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280
281 #[test]
282 fn test_advanced_spin_drift() {
283 let drift = calculate_advanced_spin_drift(
285 1.5, 1.2, 600.0, 850.0, 1500.0, 0.00308, 0.0108, 1.225, true, "match", );
296
297 assert!(drift > 0.0);
299 assert!(drift < 0.3); }
301
302 #[test]
303 fn test_transonic_correction() {
304 let subsonic = calculate_transonic_correction(0.7, 0.75);
305 let transonic = calculate_transonic_correction(1.0, 0.75);
306 let supersonic = calculate_transonic_correction(1.5, 0.75);
307
308 assert_eq!(subsonic, 1.0);
309 assert!(transonic > 0.8 && transonic < 1.0);
310 assert!(supersonic > 0.7 && supersonic < 1.0);
311 }
312
313 #[test]
314 fn test_spin_drift_direction() {
315 let right_drift = calculate_advanced_spin_drift(
317 1.5, 1.0, 700.0, 850.0, 1500.0, 0.00308, 0.0108, 1.225, true, "match",
318 );
319
320 let left_drift = calculate_advanced_spin_drift(
322 1.5, 1.0, 700.0, 850.0, 1500.0, 0.00308, 0.0108, 1.225, false, "match",
323 );
324
325 assert!(right_drift > 0.0, "Right twist should give positive drift");
326 assert!(left_drift < 0.0, "Left twist should give negative drift");
327 assert!(
328 (right_drift.abs() - left_drift.abs()).abs() < 0.001,
329 "Magnitude should be equal"
330 );
331 }
332
333 #[test]
334 fn test_spin_drift_edge_cases() {
335 let zero_time = calculate_advanced_spin_drift(
337 1.5, 0.0, 700.0, 850.0, 1500.0, 0.00308, 0.0108, 1.225, true, "match",
338 );
339 assert_eq!(zero_time, 0.0);
340
341 let zero_stability = calculate_advanced_spin_drift(
343 0.0, 1.0, 700.0, 850.0, 1500.0, 0.00308, 0.0108, 1.225, true, "match",
344 );
345 assert_eq!(zero_stability, 0.0);
346
347 let zero_muzzle_vel = calculate_advanced_spin_drift(
349 1.5, 1.0, 700.0, 0.0, 1500.0, 0.00308, 0.0108, 1.225, true, "match",
350 );
351 assert_eq!(zero_muzzle_vel, 0.0);
352
353 let zero_density = calculate_advanced_spin_drift(
355 1.5, 1.0, 700.0, 850.0, 1500.0, 0.00308, 0.0108, 0.0, true, "match",
356 );
357 assert_eq!(zero_density, 0.0);
358 }
359
360 #[test]
361 fn test_spin_drift_coefficients_bullet_types() {
362 let match_coeffs = SpinDriftCoefficients::for_bullet_type("match");
363 let vld_coeffs = SpinDriftCoefficients::for_bullet_type("vld");
364 let flat_base_coeffs = SpinDriftCoefficients::for_bullet_type("flat_base");
365 let default_coeffs = SpinDriftCoefficients::for_bullet_type("unknown");
366
367 assert!(vld_coeffs.litz_coefficient < match_coeffs.litz_coefficient);
369
370 assert!(flat_base_coeffs.litz_coefficient > match_coeffs.litz_coefficient);
372
373 assert_eq!(default_coeffs.litz_coefficient, match_coeffs.litz_coefficient);
375 }
376
377 #[test]
378 fn test_spin_drift_increases_with_time() {
379 let drift_short = calculate_advanced_spin_drift(
380 1.5, 0.5, 700.0, 850.0, 1500.0, 0.00308, 0.0108, 1.225, true, "match",
381 );
382 let drift_medium = calculate_advanced_spin_drift(
383 1.5, 1.0, 700.0, 850.0, 1500.0, 0.00308, 0.0108, 1.225, true, "match",
384 );
385 let drift_long = calculate_advanced_spin_drift(
386 1.5, 2.0, 700.0, 850.0, 1500.0, 0.00308, 0.0108, 1.225, true, "match",
387 );
388
389 assert!(drift_medium > drift_short, "Drift should increase with time");
390 assert!(drift_long > drift_medium, "Drift should increase with time");
391 }
392
393 #[test]
394 fn test_advanced_yaw_of_repose() {
395 let yaw = calculate_advanced_yaw_of_repose(
396 1.5, 800.0, 5.0, 1500.0, 1.225, 0.00782, );
403
404 assert!(yaw.abs() < 0.1, "Yaw should be small angle, got {}", yaw);
406 }
407
408 #[test]
409 fn test_yaw_of_repose_edge_cases() {
410 let zero_stability = calculate_advanced_yaw_of_repose(0.5, 800.0, 5.0, 1500.0, 1.225, 0.00782);
412 assert_eq!(zero_stability, 0.0);
413
414 let zero_velocity = calculate_advanced_yaw_of_repose(1.5, 0.0, 5.0, 1500.0, 1.225, 0.00782);
416 assert_eq!(zero_velocity, 0.0);
417
418 let no_wind = calculate_advanced_yaw_of_repose(1.5, 800.0, 0.0, 1500.0, 1.225, 0.00782);
420 assert!(no_wind > 0.0, "Should have natural yaw from trajectory curvature");
421 }
422
423 #[test]
424 fn test_transonic_correction_continuity() {
425 let just_below_transonic = calculate_transonic_correction(0.79, 0.75);
427 let just_at_transonic_start = calculate_transonic_correction(0.80, 0.75);
428
429 assert!(
431 (just_below_transonic - just_at_transonic_start).abs() < 0.01,
432 "Should be continuous at transonic start"
433 );
434
435 let just_below_supersonic = calculate_transonic_correction(1.19, 0.75);
436 let just_at_supersonic = calculate_transonic_correction(1.21, 0.75);
437
438 assert!(just_below_supersonic > 0.0);
440 assert!(just_at_supersonic > 0.0);
441 }
442
443 #[test]
444 fn test_ml_correction_placeholder() {
445 let base_drift = 0.1;
447 let corrected = apply_ml_correction(base_drift, 1.5, 2.5, 1.0, 0.308, 168.0);
448
449 assert!(corrected > 0.0);
451
452 let over_stab_subsonic = apply_ml_correction(0.1, 3.0, 0.8, 1.0, 0.308, 168.0);
455 assert!(over_stab_subsonic < 0.1, "Over-stabilized subsonic should drift less");
456
457 let long_subsonic = apply_ml_correction(0.1, 1.5, 0.85, 2.5, 0.308, 168.0);
459 assert!(long_subsonic > 0.1, "Long subsonic flight should need more correction");
460
461 let light_small = apply_ml_correction(0.1, 1.5, 2.5, 1.0, 0.224, 55.0);
463 assert!(light_small < 0.1, "Light small caliber should drift less");
464 }
465
466 #[test]
467 fn test_density_affects_drift() {
468 let sea_level = calculate_advanced_spin_drift(
470 1.5, 1.0, 700.0, 850.0, 1500.0, 0.00308, 0.0108, 1.225, true, "match",
471 );
472 let high_altitude = calculate_advanced_spin_drift(
473 1.5, 1.0, 700.0, 850.0, 1500.0, 0.00308, 0.0108, 1.0, true, "match",
474 );
475
476 assert!(
477 high_altitude > sea_level,
478 "Higher altitude (lower density) should increase drift"
479 );
480 }
481
482 #[test]
483 fn test_different_bullet_types_drift() {
484 let match_drift = calculate_advanced_spin_drift(
485 1.5, 1.0, 700.0, 850.0, 1500.0, 0.00308, 0.0108, 1.225, true, "match",
486 );
487 let vld_drift = calculate_advanced_spin_drift(
488 1.5, 1.0, 700.0, 850.0, 1500.0, 0.00308, 0.0108, 1.225, true, "vld",
489 );
490 let flat_base_drift = calculate_advanced_spin_drift(
491 1.5, 1.0, 700.0, 850.0, 1500.0, 0.00308, 0.0108, 1.225, true, "flat_base",
492 );
493
494 assert!(vld_drift < match_drift, "VLD should drift less than match");
496
497 assert!(flat_base_drift > match_drift, "Flat base should drift more");
499 }
500
501 #[test]
502 fn test_litz_drift_low_stability() {
503 let low_stability = calculate_litz_drift(0.9, 1.0, 1.25);
505 assert_eq!(low_stability, 0.0);
506
507 let exactly_one = calculate_litz_drift(1.0, 1.0, 1.25);
508 assert_eq!(exactly_one, 0.0);
509
510 let above_one = calculate_litz_drift(1.1, 1.0, 1.25);
512 assert!(above_one > 0.0);
513 }
514
515 #[test]
516 fn test_aerodynamic_jump_correction_edge_cases() {
517 let zero_mach = calculate_aerodynamic_jump_correction(0.0, 1500.0, 0.00308, 0.0108, 0.85);
519 assert_eq!(zero_mach, 0.0);
520
521 let valid = calculate_aerodynamic_jump_correction(2.5, 1500.0, 0.00308, 0.0108, 0.85);
523 assert!(valid != 0.0);
524 }
525
526 #[test]
527 fn test_yaw_damping_factor() {
528 let low_stability_damping = calculate_yaw_damping_factor(1.2, 1.0, 0.92);
530 let high_stability_damping = calculate_yaw_damping_factor(2.0, 1.0, 0.92);
531
532 assert!(high_stability_damping >= low_stability_damping);
534
535 assert!(low_stability_damping >= 0.5);
537 assert!(low_stability_damping <= 1.0);
538 assert!(high_stability_damping >= 0.5);
539 assert!(high_stability_damping <= 1.0);
540 }
541}