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(
346 0.0, 800.0, 1.225, 0.00782, 0.033, ¶ms
347 );
348 assert_eq!(zero_moment, 0.0);
349
350 let zero_vel_moment = calculate_spin_damping_moment(
352 1000.0, 0.0, 1.225, 0.00782, 0.033, ¶ms
353 );
354 assert_eq!(zero_vel_moment, 0.0);
355 }
356
357 #[test]
358 fn test_spin_decay_rate() {
359 let params = SpinDecayParameters::from_bullet_type("fmj");
360
361 let decay_rate = calculate_spin_decay_rate(
362 1000.0, 800.0, 1.225, 168.0, 0.308, 1.2, ¶ms,
369 "boat_tail",
370 );
371
372 assert!(decay_rate < 0.0);
374 assert!(decay_rate > -1000.0); }
376
377 #[test]
378 fn test_different_bullet_types() {
379 let types = ["match", "hunting", "fmj", "cast", "unknown"];
381
382 for bullet_type in &types {
383 let params = SpinDecayParameters::from_bullet_type(bullet_type);
384 assert!(params.surface_roughness > 0.0);
385 assert!(params.skin_friction_coefficient > 0.0);
386 assert!(params.form_factor > 0.0);
387 }
388 }
389
390 #[test]
391 fn test_moment_of_inertia_shapes() {
392 let mass_kg = 0.01;
393 let caliber_m = 0.008;
394 let length_m = 0.03;
395
396 let i_cylinder = calculate_moment_of_inertia(mass_kg, caliber_m, length_m, "cylinder");
397 let i_ogive = calculate_moment_of_inertia(mass_kg, caliber_m, length_m, "ogive");
398 let i_boat_tail = calculate_moment_of_inertia(mass_kg, caliber_m, length_m, "boat_tail");
399 let i_default = calculate_moment_of_inertia(mass_kg, caliber_m, length_m, "unknown");
400
401 assert!(i_cylinder > i_ogive);
403 assert!(i_ogive > i_boat_tail);
404 assert_eq!(i_cylinder, i_default); assert!(i_cylinder > 0.0);
408 assert!(i_boat_tail > 0.0);
409 }
410
411 #[test]
412 fn test_spin_decay_correction_factor() {
413 let params = SpinDecayParameters::from_bullet_type("match");
414
415 let factor_t0 = calculate_spin_decay_correction_factor(
417 0.0, 800.0, 1.225, 175.0, 0.308, 1.3, Some(¶ms)
418 );
419 assert_eq!(factor_t0, 1.0);
420
421 let factor_t3 = calculate_spin_decay_correction_factor(
423 3.0, 800.0, 1.225, 175.0, 0.308, 1.3, Some(¶ms)
424 );
425 assert!(factor_t3 < 1.0);
426 assert!(factor_t3 > 0.5);
427
428 let factor_t1 = calculate_spin_decay_correction_factor(
430 1.0, 800.0, 1.225, 175.0, 0.308, 1.3, Some(¶ms)
431 );
432 let factor_t2 = calculate_spin_decay_correction_factor(
433 2.0, 800.0, 1.225, 175.0, 0.308, 1.3, Some(¶ms)
434 );
435 assert!(factor_t1 > factor_t2);
436 assert!(factor_t2 > factor_t3);
437 }
438
439 #[test]
440 fn test_default_impl() {
441 let params1 = SpinDecayParameters::new();
442 let params2 = SpinDecayParameters::default();
443
444 assert_eq!(params1.surface_roughness, params2.surface_roughness);
445 assert_eq!(params1.skin_friction_coefficient, params2.skin_friction_coefficient);
446 assert_eq!(params1.form_factor, params2.form_factor);
447 }
448
449 #[test]
450 fn test_mass_factor_effects() {
451 let params = SpinDecayParameters::from_bullet_type("match");
452
453 let spin_light = update_spin_rate(
455 1000.0, 2.0, 800.0, 1.225, 55.0, 0.224, 0.9, Some(¶ms)
456 );
457
458 let spin_heavy = update_spin_rate(
460 1000.0, 2.0, 800.0, 1.225, 300.0, 0.338, 1.8, Some(¶ms)
461 );
462
463 assert!(spin_heavy > spin_light);
465 }
466
467 #[test]
468 fn test_velocity_factor_effects() {
469 let params = SpinDecayParameters::from_bullet_type("hunting");
470
471 let spin_low_vel = update_spin_rate(
473 1000.0, 2.0, 400.0, 1.225, 175.0, 0.308, 1.3, Some(¶ms)
474 );
475
476 let spin_high_vel = update_spin_rate(
478 1000.0, 2.0, 1200.0, 1.225, 175.0, 0.308, 1.3, Some(¶ms)
479 );
480
481 assert!(spin_low_vel > spin_high_vel);
483 }
484}