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 fn calculate_spin_rate(velocity_mps: f64, twist_rate_inches: f64) -> (f64, f64) {
25 if twist_rate_inches <= 0.0 {
26 return (0.0, 0.0);
27 }
28
29 let velocity_ips = velocity_mps * 39.3701;
31
32 let spin_rate_rps = velocity_ips / twist_rate_inches;
34
35 let spin_rate_rad_s = spin_rate_rps * 2.0 * PI;
37
38 (spin_rate_rps, spin_rate_rad_s)
39}
40
41pub fn calculate_dynamic_stability(
43 bullet_mass_grains: f64,
44 velocity_mps: f64,
45 spin_rate_rad_s: f64,
46 caliber_inches: f64,
47 length_inches: f64,
48 air_density_kg_m3: f64,
49) -> f64 {
50 if spin_rate_rad_s == 0.0 || velocity_mps == 0.0 {
51 return 0.0;
52 }
53
54 let velocity_fps = velocity_mps * 3.28084;
56
57 if caliber_inches > 0.0 {
59 let spin_rps = spin_rate_rad_s / (2.0 * PI);
61 let velocity_ips = velocity_fps * 12.0; let twist_inches = if spin_rps > 0.0 {
63 velocity_ips / spin_rps
64 } else {
65 0.0
66 };
67 let twist_calibers = if twist_inches > 0.0 {
68 twist_inches / caliber_inches
69 } else {
70 0.0
71 };
72
73 let length_calibers = if caliber_inches > 0.0 {
75 length_inches / caliber_inches
76 } else {
77 0.0
78 };
79
80 if twist_calibers == 0.0 || length_calibers == 0.0 {
86 return 0.0;
87 }
88
89 let numerator = 30.0 * bullet_mass_grains;
90 let denominator = twist_calibers.powi(2)
91 * caliber_inches.powi(3)
92 * length_calibers
93 * (1.0 + length_calibers.powi(2));
94
95 if denominator == 0.0 {
96 return 0.0;
97 }
98
99 let sg_base = numerator / denominator;
101
102 let velocity_factor = (velocity_fps / 2800.0).powf(1.0 / 3.0);
104
105 let density_factor = (1.225 / air_density_kg_m3).sqrt();
108
109 sg_base * velocity_factor * density_factor
111 } else {
112 0.0
113 }
114}
115
116pub fn calculate_yaw_of_repose(
118 stability_factor: f64,
119 velocity_mps: f64,
120 spin_rate_rad_s: f64,
121 wind_velocity_mps: f64,
122 pitch_rate_rad_s: f64,
123 air_density_kg_m3: f64,
124 caliber_inches: f64,
125 length_inches: f64,
126 mass_grains: f64,
127 mach: f64,
128 bullet_type: &str,
129 use_pitch_damping: bool,
130) -> (f64, f64) {
131 if stability_factor <= 1.0 || spin_rate_rad_s == 0.0 {
132 return (0.0, 0.0);
133 }
134
135 if use_pitch_damping && mach > 0.0 {
137 let damping_type = match bullet_type.to_lowercase().as_str() {
139 "match" => "match_boat_tail",
140 "hunting" => "hunting",
141 "fmj" => "fmj",
142 "vld" => "vld",
143 _ => "match_boat_tail",
144 };
145
146 return calculate_damped_yaw_of_repose(
147 stability_factor,
148 velocity_mps,
149 spin_rate_rad_s,
150 wind_velocity_mps,
151 pitch_rate_rad_s,
152 air_density_kg_m3,
153 caliber_inches,
154 length_inches,
155 mass_grains,
156 mach,
157 damping_type,
158 );
159 }
160
161 let yaw_rad = if wind_velocity_mps == 0.0 {
164 0.002 } else {
168 if velocity_mps > 0.0 {
170 (wind_velocity_mps / velocity_mps).atan()
171 } else {
172 0.0
173 }
174 };
175
176 let stability_term = (stability_factor - 1.0).max(0.0).sqrt();
178 let damping = 1.0 / (1.0 + stability_term);
179
180 (yaw_rad * damping, 0.0) }
182
183pub fn calculate_magnus_drift_component(
185 velocity_mps: f64,
186 spin_rate_rad_s: f64,
187 yaw_rad: f64,
188 air_density_kg_m3: f64,
189 caliber_inches: f64,
190 time_s: f64,
191 mass_grains: f64,
192) -> f64 {
193 let diameter_m = caliber_inches * 0.0254;
194 let mass_kg = mass_grains * 0.00006479891; let mach = velocity_mps / 343.0; let cmag = if mach < 0.8 {
201 0.25
202 } else if mach < 1.2 {
203 0.15
205 } else {
206 0.10 + 0.05 * ((mach - 1.2) / 2.0).min(1.0)
208 };
209
210 let spin_ratio = (spin_rate_rad_s * diameter_m / 2.0) / velocity_mps;
212
213 let magnus_force = if velocity_mps > 0.0 {
215 cmag * spin_ratio
216 * yaw_rad
217 * 0.5
218 * air_density_kg_m3
219 * velocity_mps.powi(2)
220 * PI
221 * (diameter_m / 2.0).powi(2)
222 } else {
223 0.0
224 };
225
226 let magnus_accel = magnus_force / mass_kg;
228
229 0.5 * magnus_accel * time_s.powi(2)
232}
233
234pub fn calculate_gyroscopic_drift(
236 stability_factor: f64,
237 _yaw_rad: f64,
238 velocity_mps: f64,
239 time_s: f64,
240 is_right_twist: bool,
241) -> f64 {
242 if stability_factor <= 1.0 || time_s <= 0.0 {
243 return 0.0;
244 }
245
246 let velocity_fps = velocity_mps * 3.28084;
248 if velocity_fps < 1125.0 {
249 return 0.0;
250 }
251
252 let sign = if is_right_twist { 1.0 } else { -1.0 };
254
255 let base_coefficient = 1.25 * (stability_factor + 1.2);
257 let time_factor = time_s.powf(1.83);
258 let drift_in = sign * base_coefficient * time_factor;
259
260 drift_in * 0.0254
263}
264
265pub fn calculate_enhanced_spin_drift(
267 bullet_mass: f64,
268 velocity_mps: f64,
269 twist_rate: f64,
270 bullet_diameter: f64,
271 bullet_length: f64,
272 is_twist_right: bool,
273 time_s: f64,
274 air_density: f64,
275 crosswind_mps: f64,
276 pitch_rate_rad_s: f64,
277 use_pitch_damping: bool,
278) -> SpinDriftComponents {
279 let muzzle_velocity = velocity_mps; let (_initial_spin_rps, initial_spin_rad_s) = calculate_spin_rate(muzzle_velocity, twist_rate);
282
283 let decay_params = SpinDecayParameters::from_bullet_type("match"); let current_spin_rad_s = update_spin_rate(
286 initial_spin_rad_s,
287 time_s,
288 velocity_mps,
289 air_density,
290 bullet_mass * 15.432358, bullet_diameter,
292 bullet_length,
293 Some(&decay_params),
294 );
295
296 let spin_rps = current_spin_rad_s / (2.0 * PI);
297 let spin_rad_s = current_spin_rad_s;
298
299 let stability = calculate_dynamic_stability(
301 bullet_mass,
302 velocity_mps,
303 spin_rad_s,
304 bullet_diameter,
305 bullet_length,
306 air_density,
307 );
308
309 let mach = velocity_mps / 343.0; let bullet_type = "match";
314
315 let (yaw_rad, convergence_rate) = calculate_yaw_of_repose(
317 stability,
318 velocity_mps,
319 spin_rad_s,
320 crosswind_mps,
321 pitch_rate_rad_s,
322 air_density,
323 bullet_diameter,
324 bullet_length,
325 bullet_mass,
326 mach,
327 bullet_type,
328 use_pitch_damping,
329 );
330
331 let magnus_drift = calculate_magnus_drift_component(
333 velocity_mps,
334 spin_rad_s,
335 yaw_rad,
336 air_density,
337 bullet_diameter,
338 time_s,
339 bullet_mass,
340 );
341
342 let gyro_drift =
344 calculate_gyroscopic_drift(stability, yaw_rad, velocity_mps, time_s, is_twist_right);
345
346 let total_drift = magnus_drift + gyro_drift;
348
349 let drift_rate = if time_s > 0.0 {
351 total_drift / time_s
352 } else {
353 0.0
354 };
355
356 let pitch_damping_moment = if use_pitch_damping && mach > 0.0 {
358 let coeffs = PitchDampingCoefficients::from_bullet_type(bullet_type);
359 calculate_pitch_damping_moment(
360 pitch_rate_rad_s,
361 velocity_mps,
362 air_density,
363 bullet_diameter * 0.0254, bullet_length * 0.0254, mach,
366 &coeffs,
367 )
368 } else {
369 0.0
370 };
371
372 SpinDriftComponents {
373 spin_rate_rps: spin_rps,
374 spin_rate_rad_s: spin_rad_s,
375 stability_factor: stability,
376 yaw_of_repose_rad: yaw_rad,
377 drift_rate_mps: drift_rate,
378 total_drift_m: total_drift,
379 magnus_component_m: magnus_drift,
380 gyroscopic_component_m: gyro_drift,
381 pitch_damping_moment,
382 yaw_convergence_rate: convergence_rate,
383 pitch_rate_rad_s,
384 }
385}
386
387pub fn apply_enhanced_spin_drift(
389 derivatives: &mut [f64; 6],
390 spin_components: &SpinDriftComponents,
391 time_s: f64,
392 is_right_twist: bool,
393) {
394 if time_s > 0.1 {
395 let spin_accel_z = 2.0 * spin_components.drift_rate_mps / time_s;
398
399 let sign = if is_right_twist { 1.0 } else { -1.0 };
401 derivatives[5] += sign * spin_accel_z;
402 }
403}
404
405pub fn compute_enhanced_spin_drift_simple(
407 time_s: f64,
408 stability: f64,
409 velocity_mps: f64,
410 twist_rate: f64,
411 is_twist_right: bool,
412 _caliber: f64,
413) -> f64 {
414 if twist_rate <= 0.0 {
415 return 0.0;
416 }
417
418 let (_, initial_spin_rad_s) = calculate_spin_rate(velocity_mps, twist_rate);
420
421 let decay_params = SpinDecayParameters::from_bullet_type("match");
423 let spin_rad_s = update_spin_rate(
424 initial_spin_rad_s,
425 time_s,
426 velocity_mps,
427 1.225, 175.0, _caliber,
430 1.3, Some(&decay_params),
432 );
433
434 let (yaw_rad, _) = calculate_yaw_of_repose(
436 stability,
437 velocity_mps,
438 spin_rad_s,
439 0.0,
440 0.0,
441 1.225,
442 _caliber,
443 1.3,
444 175.0,
445 velocity_mps / 343.0,
446 "match",
447 false,
448 );
449
450 calculate_gyroscopic_drift(stability, yaw_rad, velocity_mps, time_s, is_twist_right)
453}
454
455#[cfg(test)]
456mod tests {
457 use super::*;
458
459 #[test]
460 fn test_calculate_spin_rate() {
461 let (rps, rad_s) = calculate_spin_rate(800.0, 10.0);
463
464 assert!((rps - 3149.6).abs() < 1.0);
466 assert!((rad_s - rps * 2.0 * PI).abs() < 0.1);
467
468 let (rps_zero, rad_s_zero) = calculate_spin_rate(800.0, 0.0);
470 assert_eq!(rps_zero, 0.0);
471 assert_eq!(rad_s_zero, 0.0);
472 }
473
474 #[test]
475 fn test_calculate_dynamic_stability() {
476 let sg = calculate_dynamic_stability(
477 168.0, 800.0, 19792.0, 0.308, 1.2, 1.225, );
484
485 assert!(sg > 1.0);
487 assert!(sg < 10.0); }
489
490 #[test]
491 fn test_calculate_yaw_of_repose() {
492 let (yaw, _) = calculate_yaw_of_repose(
493 2.5, 800.0, 19792.0, 10.0, 0.0, 1.225, 0.308, 1.2, 168.0, 2.33, "match", false, );
506
507 assert!(yaw.abs() > 0.0);
509 assert!(yaw.abs() < 0.1); }
511
512 #[test]
513 fn test_enhanced_spin_drift_calculation() {
514 let components = calculate_enhanced_spin_drift(
515 168.0, 800.0, 10.0, 0.308, 1.2, true, 1.0, 1.225, 10.0, 0.0, false, );
527
528 assert!(components.total_drift_m.abs() > 0.0);
530 assert!(components.spin_rate_rps > 0.0);
531 assert!(components.stability_factor > 0.0);
532 }
533
534 #[test]
535 fn test_opposite_twist_directions() {
536 let right_drift = calculate_enhanced_spin_drift(
538 168.0, 800.0, 10.0, 0.308, 1.2, true, 1.0, 1.225, 0.0, 0.0, false,
539 );
540
541 let left_drift = calculate_enhanced_spin_drift(
543 168.0, 800.0, 10.0, 0.308, 1.2, false, 1.0, 1.225, 0.0, 0.0, false,
544 );
545
546 assert!(right_drift.gyroscopic_component_m * left_drift.gyroscopic_component_m < 0.0);
548 assert!(
549 (right_drift.gyroscopic_component_m.abs() - left_drift.gyroscopic_component_m.abs())
550 .abs()
551 < 0.001
552 );
553 }
554}