1use std::f64::consts::PI;
2use crate::spin_decay::{update_spin_rate, SpinDecayParameters};
3use crate::pitch_damping::{
4 calculate_damped_yaw_of_repose, PitchDampingCoefficients,
5 calculate_pitch_damping_moment,
6};
7
8#[derive(Debug, Clone)]
10pub struct SpinDriftComponents {
11 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, }
23
24pub fn calculate_spin_rate(velocity_mps: f64, twist_rate_inches: f64) -> (f64, f64) {
26 if twist_rate_inches <= 0.0 {
27 return (0.0, 0.0);
28 }
29
30 let velocity_ips = velocity_mps * 39.3701;
32
33 let spin_rate_rps = velocity_ips / twist_rate_inches;
35
36 let spin_rate_rad_s = spin_rate_rps * 2.0 * PI;
38
39 (spin_rate_rps, spin_rate_rad_s)
40}
41
42pub fn calculate_dynamic_stability(
44 bullet_mass_grains: f64,
45 velocity_mps: f64,
46 spin_rate_rad_s: f64,
47 caliber_inches: f64,
48 length_inches: f64,
49 air_density_kg_m3: f64,
50) -> f64 {
51 if spin_rate_rad_s == 0.0 || velocity_mps == 0.0 {
52 return 0.0;
53 }
54
55 let velocity_fps = velocity_mps * 3.28084;
57
58 if caliber_inches > 0.0 {
60 let spin_rps = spin_rate_rad_s / (2.0 * PI);
62 let velocity_ips = velocity_fps * 12.0; let twist_inches = if spin_rps > 0.0 { velocity_ips / spin_rps } else { 0.0 };
64 let twist_calibers = if twist_inches > 0.0 { twist_inches / caliber_inches } else { 0.0 };
65
66 let length_calibers = if caliber_inches > 0.0 { length_inches / caliber_inches } else { 0.0 };
68
69 if twist_calibers == 0.0 || length_calibers == 0.0 {
75 return 0.0;
76 }
77
78 let numerator = 30.0 * bullet_mass_grains;
79 let denominator = twist_calibers.powi(2) * caliber_inches.powi(3) *
80 length_calibers * (1.0 + length_calibers.powi(2));
81
82 if denominator == 0.0 {
83 return 0.0;
84 }
85
86 let sg_base = numerator / denominator;
88
89 let velocity_factor = (velocity_fps / 2800.0).powf(1.0 / 3.0);
91
92 let density_factor = (1.225 / air_density_kg_m3).sqrt();
95
96 sg_base * velocity_factor * density_factor
98 } else {
99 0.0
100 }
101}
102
103pub fn calculate_yaw_of_repose(
105 stability_factor: f64,
106 velocity_mps: f64,
107 spin_rate_rad_s: f64,
108 wind_velocity_mps: f64,
109 pitch_rate_rad_s: f64,
110 air_density_kg_m3: f64,
111 caliber_inches: f64,
112 length_inches: f64,
113 mass_grains: f64,
114 mach: f64,
115 bullet_type: &str,
116 use_pitch_damping: bool,
117) -> (f64, f64) {
118 if stability_factor <= 1.0 || spin_rate_rad_s == 0.0 {
119 return (0.0, 0.0);
120 }
121
122 if use_pitch_damping && mach > 0.0 {
124 let damping_type = match bullet_type.to_lowercase().as_str() {
126 "match" => "match_boat_tail",
127 "hunting" => "hunting",
128 "fmj" => "fmj",
129 "vld" => "vld",
130 _ => "match_boat_tail",
131 };
132
133 return calculate_damped_yaw_of_repose(
134 stability_factor, velocity_mps, spin_rate_rad_s,
135 wind_velocity_mps, pitch_rate_rad_s, air_density_kg_m3,
136 caliber_inches, length_inches, mass_grains, mach, damping_type
137 );
138 }
139
140 let yaw_rad = if wind_velocity_mps == 0.0 {
143 0.002 } else {
147 if velocity_mps > 0.0 {
149 (wind_velocity_mps / velocity_mps).atan()
150 } else {
151 0.0
152 }
153 };
154
155 let stability_term = (stability_factor - 1.0).max(0.0).sqrt();
157 let damping = 1.0 / (1.0 + stability_term);
158
159 (yaw_rad * damping, 0.0) }
161
162pub fn calculate_magnus_drift_component(
164 velocity_mps: f64,
165 spin_rate_rad_s: f64,
166 yaw_rad: f64,
167 air_density_kg_m3: f64,
168 caliber_inches: f64,
169 time_s: f64,
170 mass_grains: f64,
171) -> f64 {
172 let diameter_m = caliber_inches * 0.0254;
173 let mass_kg = mass_grains * 0.00006479891; let mach = velocity_mps / 343.0; let cmag = if mach < 0.8 {
180 0.25
181 } else if mach < 1.2 {
182 0.15
184 } else {
185 0.10 + 0.05 * ((mach - 1.2) / 2.0).min(1.0)
187 };
188
189 let spin_ratio = (spin_rate_rad_s * diameter_m / 2.0) / velocity_mps;
191
192 let magnus_force = if velocity_mps > 0.0 {
194 cmag * spin_ratio * yaw_rad *
195 0.5 * air_density_kg_m3 * velocity_mps.powi(2) *
196 PI * (diameter_m / 2.0).powi(2)
197 } else {
198 0.0
199 };
200
201 let magnus_accel = magnus_force / mass_kg;
203
204 0.5 * magnus_accel * time_s.powi(2)
208}
209
210pub fn calculate_gyroscopic_drift(
212 stability_factor: f64,
213 _yaw_rad: f64,
214 velocity_mps: f64,
215 time_s: f64,
216 is_right_twist: bool,
217) -> f64 {
218 if stability_factor <= 1.0 || time_s <= 0.0 {
219 return 0.0;
220 }
221
222 let velocity_fps = velocity_mps * 3.28084;
224 if velocity_fps < 1125.0 {
225 return 0.0;
226 }
227
228 let sign = if is_right_twist { 1.0 } else { -1.0 };
230
231 let base_coefficient = 1.25 * (stability_factor + 1.2);
233 let time_factor = time_s.powf(1.83);
234 let drift_in = sign * base_coefficient * time_factor;
235
236 drift_in * 0.0254
240}
241
242pub fn calculate_enhanced_spin_drift(
244 bullet_mass: f64,
245 velocity_mps: f64,
246 twist_rate: f64,
247 bullet_diameter: f64,
248 bullet_length: f64,
249 is_twist_right: bool,
250 time_s: f64,
251 air_density: f64,
252 crosswind_mps: f64,
253 pitch_rate_rad_s: f64,
254 use_pitch_damping: bool,
255) -> SpinDriftComponents {
256 let muzzle_velocity = velocity_mps; let (_initial_spin_rps, initial_spin_rad_s) = calculate_spin_rate(muzzle_velocity, twist_rate);
259
260 let decay_params = SpinDecayParameters::from_bullet_type("match"); let current_spin_rad_s = update_spin_rate(
263 initial_spin_rad_s,
264 time_s,
265 velocity_mps,
266 air_density,
267 bullet_mass * 15.432358, bullet_diameter,
269 bullet_length,
270 Some(&decay_params),
271 );
272
273 let spin_rps = current_spin_rad_s / (2.0 * PI);
274 let spin_rad_s = current_spin_rad_s;
275
276 let stability = calculate_dynamic_stability(
278 bullet_mass,
279 velocity_mps,
280 spin_rad_s,
281 bullet_diameter,
282 bullet_length,
283 air_density,
284 );
285
286 let mach = velocity_mps / 343.0; let bullet_type = "match";
291
292 let (yaw_rad, convergence_rate) = calculate_yaw_of_repose(
294 stability,
295 velocity_mps,
296 spin_rad_s,
297 crosswind_mps,
298 pitch_rate_rad_s,
299 air_density,
300 bullet_diameter,
301 bullet_length,
302 bullet_mass,
303 mach,
304 bullet_type,
305 use_pitch_damping,
306 );
307
308 let magnus_drift = calculate_magnus_drift_component(
310 velocity_mps,
311 spin_rad_s,
312 yaw_rad,
313 air_density,
314 bullet_diameter,
315 time_s,
316 bullet_mass,
317 );
318
319 let gyro_drift = calculate_gyroscopic_drift(
321 stability,
322 yaw_rad,
323 velocity_mps,
324 time_s,
325 is_twist_right,
326 );
327
328 let total_drift = magnus_drift + gyro_drift;
330
331 let drift_rate = if time_s > 0.0 {
333 total_drift / time_s
334 } else {
335 0.0
336 };
337
338 let pitch_damping_moment = if use_pitch_damping && mach > 0.0 {
340 let coeffs = PitchDampingCoefficients::from_bullet_type(bullet_type);
341 calculate_pitch_damping_moment(
342 pitch_rate_rad_s,
343 velocity_mps,
344 air_density,
345 bullet_diameter * 0.0254, bullet_length * 0.0254, mach,
348 &coeffs,
349 )
350 } else {
351 0.0
352 };
353
354 SpinDriftComponents {
355 spin_rate_rps: spin_rps,
356 spin_rate_rad_s: spin_rad_s,
357 stability_factor: stability,
358 yaw_of_repose_rad: yaw_rad,
359 drift_rate_mps: drift_rate,
360 total_drift_m: total_drift,
361 magnus_component_m: magnus_drift,
362 gyroscopic_component_m: gyro_drift,
363 pitch_damping_moment,
364 yaw_convergence_rate: convergence_rate,
365 pitch_rate_rad_s,
366 }
367}
368
369pub fn apply_enhanced_spin_drift(
371 derivatives: &mut [f64; 6],
372 spin_components: &SpinDriftComponents,
373 time_s: f64,
374 is_right_twist: bool,
375) {
376 if time_s > 0.1 {
377 let spin_accel_z = 2.0 * spin_components.drift_rate_mps / time_s;
380
381 let sign = if is_right_twist { 1.0 } else { -1.0 };
383 derivatives[5] += sign * spin_accel_z;
384 }
385}
386
387pub fn compute_enhanced_spin_drift_simple(
389 time_s: f64,
390 stability: f64,
391 velocity_mps: f64,
392 twist_rate: f64,
393 is_twist_right: bool,
394 _caliber: f64,
395) -> f64 {
396 if twist_rate <= 0.0 {
397 return 0.0;
398 }
399
400 let (_, initial_spin_rad_s) = calculate_spin_rate(velocity_mps, twist_rate);
402
403 let decay_params = SpinDecayParameters::from_bullet_type("match");
405 let spin_rad_s = update_spin_rate(
406 initial_spin_rad_s,
407 time_s,
408 velocity_mps,
409 1.225, 175.0, _caliber,
412 1.3, Some(&decay_params),
414 );
415
416 let (yaw_rad, _) = calculate_yaw_of_repose(
418 stability, velocity_mps, spin_rad_s, 0.0,
419 0.0, 1.225, _caliber, 1.3, 175.0,
420 velocity_mps / 343.0, "match", false
421 );
422
423 calculate_gyroscopic_drift(
427 stability,
428 yaw_rad,
429 velocity_mps,
430 time_s,
431 is_twist_right,
432 )
433}
434
435#[cfg(test)]
436mod tests {
437 use super::*;
438
439 #[test]
440 fn test_calculate_spin_rate() {
441 let (rps, rad_s) = calculate_spin_rate(800.0, 10.0);
443
444 assert!((rps - 3149.6).abs() < 1.0);
446 assert!((rad_s - rps * 2.0 * PI).abs() < 0.1);
447
448 let (rps_zero, rad_s_zero) = calculate_spin_rate(800.0, 0.0);
450 assert_eq!(rps_zero, 0.0);
451 assert_eq!(rad_s_zero, 0.0);
452 }
453
454 #[test]
455 fn test_calculate_dynamic_stability() {
456 let sg = calculate_dynamic_stability(
457 168.0, 800.0, 19792.0, 0.308, 1.2, 1.225 );
464
465 assert!(sg > 1.0);
467 assert!(sg < 10.0); }
469
470 #[test]
471 fn test_calculate_yaw_of_repose() {
472 let (yaw, _) = calculate_yaw_of_repose(
473 2.5, 800.0, 19792.0, 10.0, 0.0, 1.225, 0.308, 1.2, 168.0, 2.33, "match", false );
486
487 assert!(yaw.abs() > 0.0);
489 assert!(yaw.abs() < 0.1); }
491
492 #[test]
493 fn test_enhanced_spin_drift_calculation() {
494 let components = calculate_enhanced_spin_drift(
495 168.0, 800.0, 10.0, 0.308, 1.2, true, 1.0, 1.225, 10.0, 0.0, false );
507
508 assert!(components.total_drift_m.abs() > 0.0);
510 assert!(components.spin_rate_rps > 0.0);
511 assert!(components.stability_factor > 0.0);
512 }
513
514 #[test]
515 fn test_opposite_twist_directions() {
516 let right_drift = calculate_enhanced_spin_drift(
518 168.0, 800.0, 10.0, 0.308, 1.2,
519 true, 1.0, 1.225, 0.0, 0.0, false
520 );
521
522 let left_drift = calculate_enhanced_spin_drift(
524 168.0, 800.0, 10.0, 0.308, 1.2,
525 false, 1.0, 1.225, 0.0, 0.0, false
526 );
527
528 assert!(right_drift.gyroscopic_component_m * left_drift.gyroscopic_component_m < 0.0);
530 assert!((right_drift.gyroscopic_component_m.abs() - left_drift.gyroscopic_component_m.abs()).abs() < 0.001);
531 }
532}