1use crate::pitch_damping::{
2 calculate_damped_yaw_of_repose, calculate_pitch_damping_moment, PitchDampingCoefficients,
3};
4use crate::spin_decay::{update_spin_rate, SpinDecayParameters};
5use std::f64::consts::PI;
6
7#[derive(Debug, Clone)]
9pub struct SpinDriftComponents {
10 pub spin_rate_rps: f64, pub spin_rate_rad_s: f64, pub stability_factor: f64, pub yaw_of_repose_rad: f64, pub drift_rate_mps: f64, pub total_drift_m: f64, pub magnus_component_m: f64, pub gyroscopic_component_m: f64, pub pitch_damping_moment: f64, pub yaw_convergence_rate: f64, pub pitch_rate_rad_s: f64, }
22
23pub(crate) fn miller_stability(
28 caliber_in: f64,
29 weight_gr: f64,
30 twist_in: f64,
31 length_in: f64,
32) -> f64 {
33 if caliber_in <= 0.0 || weight_gr <= 0.0 || twist_in <= 0.0 || length_in <= 0.0 {
34 return 0.0;
35 }
36 let twist_cal = twist_in / caliber_in;
37 let l_cal = length_in / caliber_in;
38 let denom = twist_cal * twist_cal * caliber_in.powi(3) * l_cal * (1.0 + l_cal * l_cal);
39 if denom == 0.0 {
40 return 0.0;
41 }
42 30.0 * weight_gr / denom
43}
44
45pub fn calculate_spin_rate(velocity_mps: f64, twist_rate_inches: f64) -> (f64, f64) {
47 if twist_rate_inches <= 0.0 {
48 return (0.0, 0.0);
49 }
50
51 let velocity_ips = velocity_mps * 39.3701;
53
54 let spin_rate_rps = velocity_ips / twist_rate_inches;
56
57 let spin_rate_rad_s = spin_rate_rps * 2.0 * PI;
59
60 (spin_rate_rps, spin_rate_rad_s)
61}
62
63pub fn calculate_dynamic_stability(
65 bullet_mass_grains: f64,
66 velocity_mps: f64,
67 spin_rate_rad_s: f64,
68 caliber_inches: f64,
69 length_inches: f64,
70 air_density_kg_m3: f64,
71) -> f64 {
72 if spin_rate_rad_s == 0.0 || velocity_mps == 0.0 {
73 return 0.0;
74 }
75
76 let velocity_fps = velocity_mps * 3.28084;
78
79 if caliber_inches > 0.0 {
81 let spin_rps = spin_rate_rad_s / (2.0 * PI);
83 let velocity_ips = velocity_fps * 12.0; let twist_inches = if spin_rps > 0.0 {
85 velocity_ips / spin_rps
86 } else {
87 0.0
88 };
89 let twist_calibers = if twist_inches > 0.0 {
90 twist_inches / caliber_inches
91 } else {
92 0.0
93 };
94
95 let length_calibers = if caliber_inches > 0.0 {
97 length_inches / caliber_inches
98 } else {
99 0.0
100 };
101
102 if twist_calibers == 0.0 || length_calibers == 0.0 {
108 return 0.0;
109 }
110
111 let numerator = 30.0 * bullet_mass_grains;
112 let denominator = twist_calibers.powi(2)
113 * caliber_inches.powi(3)
114 * length_calibers
115 * (1.0 + length_calibers.powi(2));
116
117 if denominator == 0.0 {
118 return 0.0;
119 }
120
121 let sg_base = numerator / denominator;
123
124 let velocity_factor = (velocity_fps / 2800.0).powf(1.0 / 3.0);
126
127 let density_factor = (1.225 / air_density_kg_m3).sqrt();
130
131 sg_base * velocity_factor * density_factor
133 } else {
134 0.0
135 }
136}
137
138pub fn calculate_yaw_of_repose(
140 stability_factor: f64,
141 velocity_mps: f64,
142 spin_rate_rad_s: f64,
143 wind_velocity_mps: f64,
144 pitch_rate_rad_s: f64,
145 air_density_kg_m3: f64,
146 caliber_inches: f64,
147 length_inches: f64,
148 mass_grains: f64,
149 mach: f64,
150 bullet_type: &str,
151 use_pitch_damping: bool,
152) -> (f64, f64) {
153 if stability_factor <= 1.0 || spin_rate_rad_s == 0.0 {
154 return (0.0, 0.0);
155 }
156
157 if use_pitch_damping && mach > 0.0 {
159 let damping_type = match bullet_type.to_lowercase().as_str() {
161 "match" => "match_boat_tail",
162 "hunting" => "hunting",
163 "fmj" => "fmj",
164 "vld" => "vld",
165 _ => "match_boat_tail",
166 };
167
168 return calculate_damped_yaw_of_repose(
169 stability_factor,
170 velocity_mps,
171 spin_rate_rad_s,
172 wind_velocity_mps,
173 pitch_rate_rad_s,
174 air_density_kg_m3,
175 caliber_inches,
176 length_inches,
177 mass_grains,
178 mach,
179 damping_type,
180 );
181 }
182
183 let yaw_rad = if wind_velocity_mps == 0.0 {
186 0.002 } else {
190 if velocity_mps > 0.0 {
192 (wind_velocity_mps / velocity_mps).atan()
193 } else {
194 0.0
195 }
196 };
197
198 let stability_term = (stability_factor - 1.0).max(0.0).sqrt();
200 let damping = 1.0 / (1.0 + stability_term);
201
202 (yaw_rad * damping, 0.0) }
204
205pub fn calculate_magnus_drift_component(
207 velocity_mps: f64,
208 spin_rate_rad_s: f64,
209 yaw_rad: f64,
210 air_density_kg_m3: f64,
211 caliber_inches: f64,
212 time_s: f64,
213 mass_grains: f64,
214) -> f64 {
215 let diameter_m = caliber_inches * 0.0254;
216 let mass_kg = mass_grains * 0.00006479891; let mach = velocity_mps / 343.0; let cmag = if mach < 0.8 {
223 0.25
224 } else if mach < 1.2 {
225 0.15
227 } else {
228 0.10 + 0.05 * ((mach - 1.2) / 2.0).min(1.0)
230 };
231
232 let spin_ratio = (spin_rate_rad_s * diameter_m / 2.0) / velocity_mps;
234
235 let magnus_force = if velocity_mps > 0.0 {
237 cmag * spin_ratio
238 * yaw_rad
239 * 0.5
240 * air_density_kg_m3
241 * velocity_mps.powi(2)
242 * PI
243 * (diameter_m / 2.0).powi(2)
244 } else {
245 0.0
246 };
247
248 let magnus_accel = magnus_force / mass_kg;
250
251 0.5 * magnus_accel * time_s.powi(2)
254}
255
256pub fn calculate_gyroscopic_drift(
258 stability_factor: f64,
259 _yaw_rad: f64,
260 velocity_mps: f64,
261 time_s: f64,
262 is_right_twist: bool,
263) -> f64 {
264 if stability_factor <= 1.0 || time_s <= 0.0 {
265 return 0.0;
266 }
267
268 let velocity_fps = velocity_mps * 3.28084;
270 if velocity_fps < 1125.0 {
271 return 0.0;
272 }
273
274 let sign = if is_right_twist { 1.0 } else { -1.0 };
276
277 let base_coefficient = 1.25 * (stability_factor + 1.2);
279 let time_factor = time_s.powf(1.83);
280 let drift_in = sign * base_coefficient * time_factor;
281
282 drift_in * 0.0254
285}
286
287pub fn calculate_enhanced_spin_drift(
289 bullet_mass: f64,
290 velocity_mps: f64,
291 twist_rate: f64,
292 bullet_diameter: f64,
293 bullet_length: f64,
294 is_twist_right: bool,
295 time_s: f64,
296 air_density: f64,
297 crosswind_mps: f64,
298 pitch_rate_rad_s: f64,
299 use_pitch_damping: bool,
300) -> SpinDriftComponents {
301 let muzzle_velocity = velocity_mps; let (_initial_spin_rps, initial_spin_rad_s) = calculate_spin_rate(muzzle_velocity, twist_rate);
304
305 let decay_params = SpinDecayParameters::from_bullet_type("match"); let current_spin_rad_s = update_spin_rate(
308 initial_spin_rad_s,
309 time_s,
310 velocity_mps,
311 air_density,
312 bullet_mass, bullet_diameter,
314 bullet_length,
315 Some(&decay_params),
316 );
317
318 let spin_rps = current_spin_rad_s / (2.0 * PI);
319 let spin_rad_s = current_spin_rad_s;
320
321 let stability = calculate_dynamic_stability(
323 bullet_mass,
324 velocity_mps,
325 spin_rad_s,
326 bullet_diameter,
327 bullet_length,
328 air_density,
329 );
330
331 let mach = velocity_mps / 343.0; let bullet_type = "match";
336
337 let (yaw_rad, convergence_rate) = calculate_yaw_of_repose(
339 stability,
340 velocity_mps,
341 spin_rad_s,
342 crosswind_mps,
343 pitch_rate_rad_s,
344 air_density,
345 bullet_diameter,
346 bullet_length,
347 bullet_mass,
348 mach,
349 bullet_type,
350 use_pitch_damping,
351 );
352
353 let magnus_drift = calculate_magnus_drift_component(
355 velocity_mps,
356 spin_rad_s,
357 yaw_rad,
358 air_density,
359 bullet_diameter,
360 time_s,
361 bullet_mass,
362 );
363
364 let gyro_drift =
366 calculate_gyroscopic_drift(stability, yaw_rad, velocity_mps, time_s, is_twist_right);
367
368 let twist_sign = if is_twist_right { 1.0 } else { -1.0 };
373 let total_drift = twist_sign * magnus_drift + gyro_drift;
374
375 let drift_rate = if time_s > 0.0 {
377 total_drift / time_s
378 } else {
379 0.0
380 };
381
382 let pitch_damping_moment = if use_pitch_damping && mach > 0.0 {
384 let coeffs = PitchDampingCoefficients::from_bullet_type(bullet_type);
385 calculate_pitch_damping_moment(
386 pitch_rate_rad_s,
387 velocity_mps,
388 air_density,
389 bullet_diameter * 0.0254, bullet_length * 0.0254, mach,
392 &coeffs,
393 )
394 } else {
395 0.0
396 };
397
398 SpinDriftComponents {
399 spin_rate_rps: spin_rps,
400 spin_rate_rad_s: spin_rad_s,
401 stability_factor: stability,
402 yaw_of_repose_rad: yaw_rad,
403 drift_rate_mps: drift_rate,
404 total_drift_m: total_drift,
405 magnus_component_m: magnus_drift,
406 gyroscopic_component_m: gyro_drift,
407 pitch_damping_moment,
408 yaw_convergence_rate: convergence_rate,
409 pitch_rate_rad_s,
410 }
411}
412
413pub fn apply_enhanced_spin_drift(
415 derivatives: &mut [f64; 6],
416 spin_components: &SpinDriftComponents,
417 time_s: f64,
418 _is_right_twist: bool,
419) {
420 if time_s > 0.1 {
421 let spin_accel_z = 2.0 * spin_components.drift_rate_mps / time_s;
424
425 derivatives[5] += spin_accel_z;
430 }
431}
432
433pub fn compute_enhanced_spin_drift_simple(
435 time_s: f64,
436 stability: f64,
437 velocity_mps: f64,
438 twist_rate: f64,
439 is_twist_right: bool,
440 _caliber: f64,
441) -> f64 {
442 if twist_rate <= 0.0 {
443 return 0.0;
444 }
445
446 let (_, initial_spin_rad_s) = calculate_spin_rate(velocity_mps, twist_rate);
448
449 let decay_params = SpinDecayParameters::from_bullet_type("match");
451 let spin_rad_s = update_spin_rate(
452 initial_spin_rad_s,
453 time_s,
454 velocity_mps,
455 1.225, 175.0, _caliber,
458 1.3, Some(&decay_params),
460 );
461
462 let (yaw_rad, _) = calculate_yaw_of_repose(
464 stability,
465 velocity_mps,
466 spin_rad_s,
467 0.0,
468 0.0,
469 1.225,
470 _caliber,
471 1.3,
472 175.0,
473 velocity_mps / 343.0,
474 "match",
475 false,
476 );
477
478 calculate_gyroscopic_drift(stability, yaw_rad, velocity_mps, time_s, is_twist_right)
481}
482
483#[cfg(test)]
484mod tests {
485 use super::*;
486
487 #[test]
488 fn test_calculate_spin_rate() {
489 let (rps, rad_s) = calculate_spin_rate(800.0, 10.0);
491
492 assert!((rps - 3149.6).abs() < 1.0);
494 assert!((rad_s - rps * 2.0 * PI).abs() < 0.1);
495
496 let (rps_zero, rad_s_zero) = calculate_spin_rate(800.0, 0.0);
498 assert_eq!(rps_zero, 0.0);
499 assert_eq!(rad_s_zero, 0.0);
500 }
501
502 #[test]
503 fn test_calculate_dynamic_stability() {
504 let sg = calculate_dynamic_stability(
505 168.0, 800.0, 19792.0, 0.308, 1.2, 1.225, );
512
513 assert!(sg > 1.0);
515 assert!(sg < 10.0); }
517
518 #[test]
519 fn test_calculate_yaw_of_repose() {
520 let (yaw, _) = calculate_yaw_of_repose(
521 2.5, 800.0, 19792.0, 10.0, 0.0, 1.225, 0.308, 1.2, 168.0, 2.33, "match", false, );
534
535 assert!(yaw.abs() > 0.0);
537 assert!(yaw.abs() < 0.1); }
539
540 #[test]
541 fn test_enhanced_spin_drift_calculation() {
542 let components = calculate_enhanced_spin_drift(
543 168.0, 800.0, 10.0, 0.308, 1.2, true, 1.0, 1.225, 10.0, 0.0, false, );
555
556 assert!(components.total_drift_m.abs() > 0.0);
558 assert!(components.spin_rate_rps > 0.0);
559 assert!(components.stability_factor > 0.0);
560 }
561
562 #[test]
563 fn test_miller_stability_308_168gr() {
564 let sg = miller_stability(0.308, 168.0, 12.0, 1.215);
568 assert!(sg > 1.5 && sg < 2.0, "expected base Sg ~1.74, got {}", sg);
569 }
570
571 #[test]
572 fn test_miller_stability_invalid_inputs_zero() {
573 assert_eq!(miller_stability(0.0, 168.0, 12.0, 1.2), 0.0);
574 assert_eq!(miller_stability(0.308, 0.0, 12.0, 1.2), 0.0);
575 assert_eq!(miller_stability(0.308, 168.0, 0.0, 1.2), 0.0);
576 assert_eq!(miller_stability(0.308, 168.0, 12.0, 0.0), 0.0);
577 }
578
579 #[test]
580 fn test_opposite_twist_directions() {
581 let right_drift = calculate_enhanced_spin_drift(
583 168.0, 800.0, 10.0, 0.308, 1.2, true, 1.0, 1.225, 0.0, 0.0, false,
584 );
585
586 let left_drift = calculate_enhanced_spin_drift(
588 168.0, 800.0, 10.0, 0.308, 1.2, false, 1.0, 1.225, 0.0, 0.0, false,
589 );
590
591 assert!(right_drift.gyroscopic_component_m * left_drift.gyroscopic_component_m < 0.0);
593 assert!(
594 (right_drift.gyroscopic_component_m.abs() - left_drift.gyroscopic_component_m.abs())
595 .abs()
596 < 0.001
597 );
598 }
599
600 #[test]
601 fn test_applied_spin_drift_flips_with_twist() {
602 let time_s = 1.0;
608 let right = calculate_enhanced_spin_drift(
609 168.0, 800.0, 10.0, 0.308, 1.2, true, time_s, 1.225, 0.0, 0.0, false,
610 );
611 let left = calculate_enhanced_spin_drift(
612 168.0, 800.0, 10.0, 0.308, 1.2, false, time_s, 1.225, 0.0, 0.0, false,
613 );
614
615 let mut d_right = [0.0_f64; 6];
616 let mut d_left = [0.0_f64; 6];
617 apply_enhanced_spin_drift(&mut d_right, &right, time_s, true);
618 apply_enhanced_spin_drift(&mut d_left, &left, time_s, false);
619
620 assert!(d_right[5].abs() > 0.0, "expected non-zero spin drift accel");
621 assert!(d_left[5].abs() > 0.0, "expected non-zero spin drift accel");
622 assert!(
623 d_right[5] * d_left[5] < 0.0,
624 "expected opposite-sign lateral accel for opposite twist, got {} and {}",
625 d_right[5],
626 d_left[5]
627 );
628 }
629}