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 base_decay_rate = if let Some(params) = decay_params {
202 if params.form_factor < 1.0 {
203 0.025 } else {
206 0.04 }
209 } else {
210 0.04 };
212
213 let decay_rate_per_second = base_decay_rate * mass_factor * velocity_factor;
215
216 let decay_factor = (-decay_rate_per_second * time_elapsed_s).exp();
218
219 initial_spin_rad_s * decay_factor.clamp(0.5, 1.0)
221}
222
223pub fn calculate_spin_decay_correction_factor(
228 time_elapsed_s: f64,
229 velocity_mps: f64,
230 air_density_kg_m3: f64,
231 mass_grains: f64,
232 caliber_inches: f64,
233 length_inches: f64,
234 decay_params: Option<&SpinDecayParameters>,
235) -> f64 {
236 if time_elapsed_s <= 0.0 {
237 return 1.0;
238 }
239
240 let initial_spin = 1000.0; let current_spin = update_spin_rate(
244 initial_spin,
245 time_elapsed_s,
246 velocity_mps,
247 air_density_kg_m3,
248 mass_grains,
249 caliber_inches,
250 length_inches,
251 decay_params,
252 );
253
254 current_spin / initial_spin
255}
256
257#[cfg(test)]
258mod tests {
259 use super::*;
260
261 #[test]
262 fn test_spin_decay_parameters() {
263 let match_params = SpinDecayParameters::from_bullet_type("match");
264 assert_eq!(match_params.form_factor, 0.9);
265 assert_eq!(match_params.surface_roughness, 0.00005);
266
267 let hunting_params = SpinDecayParameters::from_bullet_type("hunting");
268 assert_eq!(hunting_params.form_factor, 1.0);
269 }
270
271 #[test]
272 fn test_moment_of_inertia() {
273 let mass_kg = 0.01134; let caliber_m = 0.00782; let i_cylinder = calculate_moment_of_inertia(mass_kg, caliber_m, 0.033, "cylinder");
277 let i_ogive = calculate_moment_of_inertia(mass_kg, caliber_m, 0.033, "ogive");
278
279 assert!(i_cylinder > i_ogive); }
281
282 #[test]
283 fn test_spin_decay_realistic() {
284 let initial_spin = 2800.0 * 2.0 * PI; let params = SpinDecayParameters::from_bullet_type("match");
287
288 let spin_after_3s = update_spin_rate(
290 initial_spin,
291 3.0, 750.0, 1.2, 175.0, 0.308, 1.3, Some(¶ms),
298 );
299
300 let decay_percent = (1.0 - spin_after_3s / initial_spin) * 100.0;
301
302 assert!(decay_percent > 2.0 && decay_percent < 15.0);
304 }
305
306 #[test]
307 fn test_spin_decay_bounds() {
308 let initial_spin = 1000.0;
309 let params = SpinDecayParameters::new();
310
311 let spin_long_time = update_spin_rate(
313 initial_spin,
314 100.0, 500.0,
316 1.225,
317 150.0,
318 0.308,
319 1.2,
320 Some(¶ms),
321 );
322
323 assert!(spin_long_time >= initial_spin * 0.5);
324 }
325
326 #[test]
327 fn test_spin_damping_moment() {
328 let params = SpinDecayParameters::from_bullet_type("match");
329
330 let moment = calculate_spin_damping_moment(
332 1000.0, 800.0, 1.225, 0.00782, 0.033, ¶ms,
338 );
339
340 assert!(moment > 0.0);
342 assert!(moment < 1.0); let zero_moment = calculate_spin_damping_moment(0.0, 800.0, 1.225, 0.00782, 0.033, ¶ms);
346 assert_eq!(zero_moment, 0.0);
347
348 let zero_vel_moment =
350 calculate_spin_damping_moment(1000.0, 0.0, 1.225, 0.00782, 0.033, ¶ms);
351 assert_eq!(zero_vel_moment, 0.0);
352 }
353
354 #[test]
355 fn test_spin_decay_rate() {
356 let params = SpinDecayParameters::from_bullet_type("fmj");
357
358 let decay_rate = calculate_spin_decay_rate(
359 1000.0, 800.0, 1.225, 168.0, 0.308, 1.2, ¶ms,
366 "boat_tail",
367 );
368
369 assert!(decay_rate < 0.0);
371 assert!(decay_rate > -1000.0); }
373
374 #[test]
375 fn test_different_bullet_types() {
376 let types = ["match", "hunting", "fmj", "cast", "unknown"];
378
379 for bullet_type in &types {
380 let params = SpinDecayParameters::from_bullet_type(bullet_type);
381 assert!(params.surface_roughness > 0.0);
382 assert!(params.skin_friction_coefficient > 0.0);
383 assert!(params.form_factor > 0.0);
384 }
385 }
386
387 #[test]
388 fn test_moment_of_inertia_shapes() {
389 let mass_kg = 0.01;
390 let caliber_m = 0.008;
391 let length_m = 0.03;
392
393 let i_cylinder = calculate_moment_of_inertia(mass_kg, caliber_m, length_m, "cylinder");
394 let i_ogive = calculate_moment_of_inertia(mass_kg, caliber_m, length_m, "ogive");
395 let i_boat_tail = calculate_moment_of_inertia(mass_kg, caliber_m, length_m, "boat_tail");
396 let i_default = calculate_moment_of_inertia(mass_kg, caliber_m, length_m, "unknown");
397
398 assert!(i_cylinder > i_ogive);
400 assert!(i_ogive > i_boat_tail);
401 assert_eq!(i_cylinder, i_default); assert!(i_cylinder > 0.0);
405 assert!(i_boat_tail > 0.0);
406 }
407
408 #[test]
409 fn test_spin_decay_correction_factor() {
410 let params = SpinDecayParameters::from_bullet_type("match");
411
412 let factor_t0 = calculate_spin_decay_correction_factor(
414 0.0,
415 800.0,
416 1.225,
417 175.0,
418 0.308,
419 1.3,
420 Some(¶ms),
421 );
422 assert_eq!(factor_t0, 1.0);
423
424 let factor_t3 = calculate_spin_decay_correction_factor(
426 3.0,
427 800.0,
428 1.225,
429 175.0,
430 0.308,
431 1.3,
432 Some(¶ms),
433 );
434 assert!(factor_t3 < 1.0);
435 assert!(factor_t3 > 0.5);
436
437 let factor_t1 = calculate_spin_decay_correction_factor(
439 1.0,
440 800.0,
441 1.225,
442 175.0,
443 0.308,
444 1.3,
445 Some(¶ms),
446 );
447 let factor_t2 = calculate_spin_decay_correction_factor(
448 2.0,
449 800.0,
450 1.225,
451 175.0,
452 0.308,
453 1.3,
454 Some(¶ms),
455 );
456 assert!(factor_t1 > factor_t2);
457 assert!(factor_t2 > factor_t3);
458 }
459
460 #[test]
461 fn test_default_impl() {
462 let params1 = SpinDecayParameters::new();
463 let params2 = SpinDecayParameters::default();
464
465 assert_eq!(params1.surface_roughness, params2.surface_roughness);
466 assert_eq!(
467 params1.skin_friction_coefficient,
468 params2.skin_friction_coefficient
469 );
470 assert_eq!(params1.form_factor, params2.form_factor);
471 }
472
473 #[test]
474 fn test_mass_factor_effects() {
475 let params = SpinDecayParameters::from_bullet_type("match");
476
477 let spin_light =
479 update_spin_rate(1000.0, 2.0, 800.0, 1.225, 55.0, 0.224, 0.9, Some(¶ms));
480
481 let spin_heavy =
483 update_spin_rate(1000.0, 2.0, 800.0, 1.225, 300.0, 0.338, 1.8, Some(¶ms));
484
485 assert!(spin_heavy > spin_light);
487 }
488
489 #[test]
490 fn test_velocity_factor_effects() {
491 let params = SpinDecayParameters::from_bullet_type("hunting");
492
493 let spin_low_vel =
495 update_spin_rate(1000.0, 2.0, 400.0, 1.225, 175.0, 0.308, 1.3, Some(¶ms));
496
497 let spin_high_vel =
499 update_spin_rate(1000.0, 2.0, 1200.0, 1.225, 175.0, 0.308, 1.3, Some(¶ms));
500
501 assert!(spin_low_vel > spin_high_vel);
503 }
504}