1use std::f64::consts::PI;
10
11#[derive(Debug, Clone, Copy)]
13pub struct SpinDecayParameters {
14 pub surface_roughness: f64,
16 pub skin_friction_coefficient: f64,
18 pub form_factor: f64,
20}
21
22impl SpinDecayParameters {
23 pub fn new() -> Self {
25 Self {
26 surface_roughness: 0.0001,
27 skin_friction_coefficient: 0.00001,
28 form_factor: 1.0,
29 }
30 }
31
32 pub fn from_bullet_type(bullet_type: &str) -> Self {
34 match bullet_type.to_lowercase().as_str() {
35 "match" => Self {
36 surface_roughness: 0.00005,
37 skin_friction_coefficient: 0.000008,
38 form_factor: 0.9,
39 },
40 "hunting" => Self {
41 surface_roughness: 0.0001,
42 skin_friction_coefficient: 0.00001,
43 form_factor: 1.0,
44 },
45 "fmj" => Self {
46 surface_roughness: 0.00015,
47 skin_friction_coefficient: 0.000012,
48 form_factor: 1.1,
49 },
50 "cast" => Self {
51 surface_roughness: 0.0002,
52 skin_friction_coefficient: 0.000015,
53 form_factor: 1.2,
54 },
55 _ => Self::new(),
56 }
57 }
58}
59
60impl Default for SpinDecayParameters {
61 fn default() -> Self {
62 Self::new()
63 }
64}
65
66pub fn calculate_spin_damping_moment(
73 spin_rate_rad_s: f64,
74 velocity_mps: f64,
75 air_density_kg_m3: f64,
76 caliber_m: f64,
77 length_m: f64,
78 decay_params: &SpinDecayParameters,
79) -> f64 {
80 if spin_rate_rad_s == 0.0 || velocity_mps == 0.0 {
81 return 0.0;
82 }
83
84 let radius = caliber_m / 2.0;
86 let tangential_velocity = spin_rate_rad_s * radius;
87 let _re_spin = air_density_kg_m3 * tangential_velocity * caliber_m / 1.81e-5; let cf = decay_params.skin_friction_coefficient;
91
92 let surface_area = PI * caliber_m * length_m;
94
95 let f_tangential = 0.5 * air_density_kg_m3 * cf * surface_area * tangential_velocity.powi(2);
97
98 let moment_skin = f_tangential * radius * decay_params.form_factor;
100
101 let spin_ratio = tangential_velocity / velocity_mps.max(1.0);
103 let magnus_damping_factor = 0.01 * spin_ratio; let moment_magnus = magnus_damping_factor * moment_skin;
105
106 moment_skin + moment_magnus
108}
109
110pub fn calculate_moment_of_inertia(
112 mass_kg: f64,
113 caliber_m: f64,
114 _length_m: f64,
115 shape: &str,
116) -> f64 {
117 let radius = caliber_m / 2.0;
118
119 match shape {
120 "cylinder" => {
121 0.5 * mass_kg * radius.powi(2)
123 }
124 "ogive" => {
125 0.4 * mass_kg * radius.powi(2)
127 }
128 "boat_tail" => {
129 0.35 * mass_kg * radius.powi(2)
131 }
132 _ => {
133 0.5 * mass_kg * radius.powi(2)
135 }
136 }
137}
138
139pub fn calculate_spin_decay_rate(
141 spin_rate_rad_s: f64,
142 velocity_mps: f64,
143 air_density_kg_m3: f64,
144 mass_grains: f64,
145 caliber_inches: f64,
146 length_inches: f64,
147 decay_params: &SpinDecayParameters,
148 bullet_shape: &str,
149) -> f64 {
150 let mass_kg = mass_grains * 0.00006479891; let caliber_m = caliber_inches * 0.0254;
153 let length_m = length_inches * 0.0254;
154
155 let damping_moment = calculate_spin_damping_moment(
157 spin_rate_rad_s,
158 velocity_mps,
159 air_density_kg_m3,
160 caliber_m,
161 length_m,
162 decay_params,
163 );
164
165 let moment_of_inertia = calculate_moment_of_inertia(mass_kg, caliber_m, length_m, bullet_shape);
167
168 if moment_of_inertia > 0.0 {
170 -damping_moment / moment_of_inertia
171 } else {
172 0.0
173 }
174}
175
176pub fn update_spin_rate(
181 initial_spin_rad_s: f64,
182 time_elapsed_s: f64,
183 velocity_mps: f64,
184 air_density_kg_m3: f64,
185 mass_grains: f64,
186 caliber_inches: f64,
187 length_inches: f64,
188 decay_params: Option<&SpinDecayParameters>,
189) -> f64 {
190 if time_elapsed_s <= 0.0 {
191 return initial_spin_rad_s;
192 }
193
194 let mass_factor = (175.0 / mass_grains).sqrt(); let velocity_factor = velocity_mps / 850.0; let density_factor = if air_density_kg_m3 > 0.0 {
203 air_density_kg_m3 / 1.225
204 } else {
205 1.0 };
207
208 let ref_surface = PI * 0.308 * 1.3; let surface_factor = if caliber_inches > 0.0 && length_inches > 0.0 {
215 let bullet_surface = PI * caliber_inches * length_inches;
216 (bullet_surface / ref_surface).sqrt()
217 } else {
218 1.0 };
220
221 let base_decay_rate = if let Some(params) = decay_params {
223 if params.form_factor < 1.0 {
224 0.025 } else {
227 0.04 }
230 } else {
231 0.04 };
233
234 let decay_rate_per_second =
238 base_decay_rate * mass_factor * velocity_factor * density_factor * surface_factor;
239
240 let decay_factor = (-decay_rate_per_second * time_elapsed_s).exp();
242
243 initial_spin_rad_s * decay_factor.clamp(0.5, 1.0)
245}
246
247pub fn calculate_spin_decay_correction_factor(
252 time_elapsed_s: f64,
253 velocity_mps: f64,
254 air_density_kg_m3: f64,
255 mass_grains: f64,
256 caliber_inches: f64,
257 length_inches: f64,
258 decay_params: Option<&SpinDecayParameters>,
259) -> f64 {
260 if time_elapsed_s <= 0.0 {
261 return 1.0;
262 }
263
264 let initial_spin = 1000.0; let current_spin = update_spin_rate(
268 initial_spin,
269 time_elapsed_s,
270 velocity_mps,
271 air_density_kg_m3,
272 mass_grains,
273 caliber_inches,
274 length_inches,
275 decay_params,
276 );
277
278 current_spin / initial_spin
279}
280
281#[cfg(test)]
282mod tests {
283 use super::*;
284
285 #[test]
286 fn test_spin_decay_parameters() {
287 let match_params = SpinDecayParameters::from_bullet_type("match");
288 assert_eq!(match_params.form_factor, 0.9);
289 assert_eq!(match_params.surface_roughness, 0.00005);
290
291 let hunting_params = SpinDecayParameters::from_bullet_type("hunting");
292 assert_eq!(hunting_params.form_factor, 1.0);
293 }
294
295 #[test]
296 fn test_moment_of_inertia() {
297 let mass_kg = 0.01134; let caliber_m = 0.00782; let i_cylinder = calculate_moment_of_inertia(mass_kg, caliber_m, 0.033, "cylinder");
301 let i_ogive = calculate_moment_of_inertia(mass_kg, caliber_m, 0.033, "ogive");
302
303 assert!(i_cylinder > i_ogive); }
305
306 #[test]
307 fn test_spin_decay_realistic() {
308 let initial_spin = 2800.0 * 2.0 * PI; let params = SpinDecayParameters::from_bullet_type("match");
311
312 let spin_after_3s = update_spin_rate(
314 initial_spin,
315 3.0, 750.0, 1.2, 175.0, 0.308, 1.3, Some(¶ms),
322 );
323
324 let decay_percent = (1.0 - spin_after_3s / initial_spin) * 100.0;
325
326 assert!(decay_percent > 2.0 && decay_percent < 15.0);
328 }
329
330 #[test]
331 fn test_spin_decay_bounds() {
332 let initial_spin = 1000.0;
333 let params = SpinDecayParameters::new();
334
335 let spin_long_time = update_spin_rate(
337 initial_spin,
338 100.0, 500.0,
340 1.225,
341 150.0,
342 0.308,
343 1.2,
344 Some(¶ms),
345 );
346
347 assert!(spin_long_time >= initial_spin * 0.5);
348 }
349
350 #[test]
351 fn test_spin_damping_moment() {
352 let params = SpinDecayParameters::from_bullet_type("match");
353
354 let moment = calculate_spin_damping_moment(
356 1000.0, 800.0, 1.225, 0.00782, 0.033, ¶ms,
362 );
363
364 assert!(moment > 0.0);
366 assert!(moment < 1.0); let zero_moment = calculate_spin_damping_moment(0.0, 800.0, 1.225, 0.00782, 0.033, ¶ms);
370 assert_eq!(zero_moment, 0.0);
371
372 let zero_vel_moment =
374 calculate_spin_damping_moment(1000.0, 0.0, 1.225, 0.00782, 0.033, ¶ms);
375 assert_eq!(zero_vel_moment, 0.0);
376 }
377
378 #[test]
379 fn test_spin_decay_rate() {
380 let params = SpinDecayParameters::from_bullet_type("fmj");
381
382 let decay_rate = calculate_spin_decay_rate(
383 1000.0, 800.0, 1.225, 168.0, 0.308, 1.2, ¶ms,
390 "boat_tail",
391 );
392
393 assert!(decay_rate < 0.0);
395 assert!(decay_rate > -1000.0); }
397
398 #[test]
399 fn test_different_bullet_types() {
400 let types = ["match", "hunting", "fmj", "cast", "unknown"];
402
403 for bullet_type in &types {
404 let params = SpinDecayParameters::from_bullet_type(bullet_type);
405 assert!(params.surface_roughness > 0.0);
406 assert!(params.skin_friction_coefficient > 0.0);
407 assert!(params.form_factor > 0.0);
408 }
409 }
410
411 #[test]
412 fn test_moment_of_inertia_shapes() {
413 let mass_kg = 0.01;
414 let caliber_m = 0.008;
415 let length_m = 0.03;
416
417 let i_cylinder = calculate_moment_of_inertia(mass_kg, caliber_m, length_m, "cylinder");
418 let i_ogive = calculate_moment_of_inertia(mass_kg, caliber_m, length_m, "ogive");
419 let i_boat_tail = calculate_moment_of_inertia(mass_kg, caliber_m, length_m, "boat_tail");
420 let i_default = calculate_moment_of_inertia(mass_kg, caliber_m, length_m, "unknown");
421
422 assert!(i_cylinder > i_ogive);
424 assert!(i_ogive > i_boat_tail);
425 assert_eq!(i_cylinder, i_default); assert!(i_cylinder > 0.0);
429 assert!(i_boat_tail > 0.0);
430 }
431
432 #[test]
433 fn test_spin_decay_correction_factor() {
434 let params = SpinDecayParameters::from_bullet_type("match");
435
436 let factor_t0 = calculate_spin_decay_correction_factor(
438 0.0,
439 800.0,
440 1.225,
441 175.0,
442 0.308,
443 1.3,
444 Some(¶ms),
445 );
446 assert_eq!(factor_t0, 1.0);
447
448 let factor_t3 = calculate_spin_decay_correction_factor(
450 3.0,
451 800.0,
452 1.225,
453 175.0,
454 0.308,
455 1.3,
456 Some(¶ms),
457 );
458 assert!(factor_t3 < 1.0);
459 assert!(factor_t3 > 0.5);
460
461 let factor_t1 = calculate_spin_decay_correction_factor(
463 1.0,
464 800.0,
465 1.225,
466 175.0,
467 0.308,
468 1.3,
469 Some(¶ms),
470 );
471 let factor_t2 = calculate_spin_decay_correction_factor(
472 2.0,
473 800.0,
474 1.225,
475 175.0,
476 0.308,
477 1.3,
478 Some(¶ms),
479 );
480 assert!(factor_t1 > factor_t2);
481 assert!(factor_t2 > factor_t3);
482 }
483
484 #[test]
485 fn test_default_impl() {
486 let params1 = SpinDecayParameters::new();
487 let params2 = SpinDecayParameters::default();
488
489 assert_eq!(params1.surface_roughness, params2.surface_roughness);
490 assert_eq!(
491 params1.skin_friction_coefficient,
492 params2.skin_friction_coefficient
493 );
494 assert_eq!(params1.form_factor, params2.form_factor);
495 }
496
497 #[test]
498 fn test_mass_factor_effects() {
499 let params = SpinDecayParameters::from_bullet_type("match");
500
501 let spin_light =
503 update_spin_rate(1000.0, 2.0, 800.0, 1.225, 55.0, 0.224, 0.9, Some(¶ms));
504
505 let spin_heavy =
507 update_spin_rate(1000.0, 2.0, 800.0, 1.225, 300.0, 0.338, 1.8, Some(¶ms));
508
509 assert!(spin_heavy > spin_light);
511 }
512
513 #[test]
514 fn test_velocity_factor_effects() {
515 let params = SpinDecayParameters::from_bullet_type("hunting");
516
517 let spin_low_vel =
519 update_spin_rate(1000.0, 2.0, 400.0, 1.225, 175.0, 0.308, 1.3, Some(¶ms));
520
521 let spin_high_vel =
523 update_spin_rate(1000.0, 2.0, 1200.0, 1.225, 175.0, 0.308, 1.3, Some(¶ms));
524
525 assert!(spin_low_vel > spin_high_vel);
527 }
528}