1#![allow(dead_code)]
9
10#[derive(Debug, Clone)]
12pub struct StabilityParameters {
13 pub nose_shape_factor: f64,
15 pub boat_tail_factor: f64,
17 pub plastic_tip_factor: f64,
19 pub cop_adjustment: f64,
21}
22
23impl StabilityParameters {
24 pub fn for_bullet_type(bullet_type: &str, has_boat_tail: bool, has_plastic_tip: bool) -> Self {
25 let mut params = match bullet_type.to_lowercase().as_str() {
26 "match" | "bthp" => Self {
27 nose_shape_factor: 0.95,
28 boat_tail_factor: if has_boat_tail { 0.94 } else { 1.0 },
29 plastic_tip_factor: 1.0,
30 cop_adjustment: 0.98,
31 },
32 "vld" | "very_low_drag" => Self {
33 nose_shape_factor: 0.88,
34 boat_tail_factor: if has_boat_tail { 0.92 } else { 1.0 },
35 plastic_tip_factor: 1.0,
36 cop_adjustment: 0.96,
37 },
38 "hybrid" => Self {
39 nose_shape_factor: 0.91,
40 boat_tail_factor: if has_boat_tail { 0.93 } else { 1.0 },
41 plastic_tip_factor: 1.0,
42 cop_adjustment: 0.97,
43 },
44 "hunting" => Self {
45 nose_shape_factor: 0.98,
46 boat_tail_factor: if has_boat_tail { 0.95 } else { 1.0 },
47 plastic_tip_factor: if has_plastic_tip { 0.92 } else { 1.0 },
48 cop_adjustment: 0.99,
49 },
50 _ => Self::default(),
51 };
52
53 if has_plastic_tip && params.plastic_tip_factor == 1.0 {
54 params.plastic_tip_factor = 0.95;
55 }
56
57 params
58 }
59
60 pub fn default() -> Self {
61 Self {
62 nose_shape_factor: 1.0,
63 boat_tail_factor: 1.0,
64 plastic_tip_factor: 1.0,
65 cop_adjustment: 1.0,
66 }
67 }
68}
69
70pub fn calculate_advanced_stability(
72 mass_grains: f64,
73 velocity_fps: f64,
74 twist_rate_inches: f64,
75 caliber_inches: f64,
76 length_inches: f64,
77 air_density_kg_m3: f64,
78 temperature_k: f64,
79 bullet_type: &str,
80 has_boat_tail: bool,
81 has_plastic_tip: bool,
82) -> f64 {
83 if twist_rate_inches == 0.0 || caliber_inches == 0.0 || length_inches == 0.0 {
84 return 0.0;
85 }
86
87 let params = StabilityParameters::for_bullet_type(bullet_type, has_boat_tail, has_plastic_tip);
88
89 let sg_base = calculate_miller_refined(
91 mass_grains,
92 twist_rate_inches,
93 caliber_inches,
94 length_inches,
95 params.nose_shape_factor,
96 );
97
98 let sg_velocity_corrected = apply_velocity_correction(sg_base, velocity_fps);
100
101 let sg_atmosphere_corrected =
103 apply_atmospheric_correction(sg_velocity_corrected, air_density_kg_m3, temperature_k);
104
105 let sg_boat_tail = sg_atmosphere_corrected * params.boat_tail_factor;
107
108 let sg_plastic_tip = sg_boat_tail * params.plastic_tip_factor;
110
111 let sg_final = sg_plastic_tip * params.cop_adjustment;
113
114 if velocity_fps > 3000.0 {
116 apply_bowman_howell_correction(sg_final, velocity_fps, caliber_inches)
117 } else {
118 sg_final
119 }
120}
121
122fn calculate_miller_refined(
124 mass_grains: f64,
125 twist_rate_inches: f64,
126 caliber_inches: f64,
127 length_inches: f64,
128 nose_shape_factor: f64,
129) -> f64 {
130 let twist_calibers = twist_rate_inches / caliber_inches;
132 let length_calibers = length_inches / caliber_inches;
133
134 const MILLER_CONSTANT: f64 = 30.0;
136
137 let inertia_factor = 1.0 + length_calibers.powi(2);
140
141 let numerator = MILLER_CONSTANT * mass_grains * nose_shape_factor;
143 let denominator =
144 twist_calibers.powi(2) * caliber_inches.powi(3) * length_calibers * inertia_factor;
145
146 if denominator == 0.0 {
147 return 0.0;
148 }
149
150 numerator / denominator
151}
152
153fn apply_velocity_correction(sg_base: f64, velocity_fps: f64) -> f64 {
155 const VELOCITY_REFERENCE: f64 = 2800.0;
158
159 if velocity_fps < 1400.0 {
161 let velocity_factor = (velocity_fps / 1400.0).powf(0.5);
162 sg_base * velocity_factor * 0.5 } else {
164 let velocity_factor = (velocity_fps / VELOCITY_REFERENCE).powf(1.0 / 3.0);
166 sg_base * velocity_factor
167 }
168}
169
170fn apply_atmospheric_correction(sg: f64, air_density_kg_m3: f64, temperature_k: f64) -> f64 {
172 const STD_DENSITY: f64 = 1.225; const STD_TEMP: f64 = 288.15; let density_ratio = STD_DENSITY / air_density_kg_m3;
180 let density_correction = density_ratio;
181
182 let temp_ratio = temperature_k / STD_TEMP;
184 let temp_correction = temp_ratio.powf(0.17); sg * density_correction * temp_correction
187}
188
189fn apply_bowman_howell_correction(sg: f64, velocity_fps: f64, caliber_inches: f64) -> f64 {
191 if velocity_fps <= 3000.0 {
193 return sg;
194 }
195
196 let excess_velocity = (velocity_fps - 3000.0) / 1000.0;
198 let mach_correction = 1.0 - 0.05 * excess_velocity.min(2.0);
199
200 let caliber_factor = if caliber_inches < 0.264 {
202 0.95
203 } else if caliber_inches < 0.308 {
204 0.97
205 } else {
206 1.0
207 };
208
209 sg * mach_correction * caliber_factor
210}
211
212pub fn calculate_dynamic_stability(
214 static_stability: f64,
215 velocity_mps: f64,
216 spin_rate_rad_s: f64,
217 yaw_angle_rad: f64,
218 caliber_m: f64,
219 _mass_kg: f64,
220) -> f64 {
221 let spin_param = if velocity_mps > 0.0 {
225 spin_rate_rad_s * caliber_m / (2.0 * velocity_mps)
226 } else {
227 0.0
228 };
229
230 let yaw_factor = 1.0 - 0.1 * yaw_angle_rad.abs().min(0.1);
232
233 let precession_factor = 1.0 + 0.05 * spin_param.min(0.5);
235
236 static_stability * yaw_factor * precession_factor
237}
238
239pub fn predict_stability_at_distance(
241 initial_stability: f64,
242 initial_velocity_fps: f64,
243 current_velocity_fps: f64,
244 spin_decay_factor: f64, ) -> f64 {
246 if initial_velocity_fps == 0.0 || current_velocity_fps == 0.0 {
247 return initial_stability;
248 }
249
250 let velocity_ratio = current_velocity_fps / initial_velocity_fps;
252
253 let spin_ratio = velocity_ratio * spin_decay_factor;
255
256 let stability_ratio = spin_ratio.powi(2) / velocity_ratio;
259
260 initial_stability * stability_ratio
261}
262
263pub fn check_trajectory_stability(
265 muzzle_stability: f64,
266 muzzle_velocity_fps: f64,
267 terminal_velocity_fps: f64,
268 spin_decay_factor: f64,
269) -> (bool, f64, String) {
270 let terminal_stability = predict_stability_at_distance(
271 muzzle_stability,
272 muzzle_velocity_fps,
273 terminal_velocity_fps,
274 spin_decay_factor,
275 );
276
277 let is_stable = terminal_stability >= 1.3; let status = if terminal_stability < 1.0 {
280 "UNSTABLE - Bullet will tumble".to_string()
281 } else if terminal_stability < 1.3 {
282 "MARGINAL - May experience accuracy issues".to_string()
283 } else if terminal_stability < 1.5 {
284 "ADEQUATE - Acceptable for most conditions".to_string()
285 } else if terminal_stability < 2.5 {
286 "GOOD - Optimal stability".to_string()
287 } else {
288 "OVER-STABILIZED - May reduce BC slightly".to_string()
289 };
290
291 (is_stable, terminal_stability, status)
292}
293
294#[cfg(test)]
295mod tests {
296 use super::*;
297
298 #[test]
299 fn test_advanced_stability() {
300 let stability = calculate_advanced_stability(
302 168.0, 2700.0, 10.0, 0.308, 1.24, 1.225, 288.15, "match", true, false, );
313
314 println!("Calculated stability: {}", stability);
315
316 assert!(stability > 1.3);
318 assert!(
319 stability < 2.5,
320 "Stability {} exceeds upper bound",
321 stability
322 );
323 }
324
325 #[test]
326 fn test_stability_prediction() {
327 let (is_stable, terminal_sg, status) = check_trajectory_stability(
329 2.2, 2700.0, 1900.0, 0.98, );
334
335 println!(
336 "is_stable: {}, terminal_sg: {}, status: {}",
337 is_stable, terminal_sg, status
338 );
339
340 assert!(
341 is_stable,
342 "Expected stable trajectory but got: is_stable={}, terminal_sg={}, status={}",
343 is_stable, terminal_sg, status
344 );
345 assert!(terminal_sg > 1.0, "Terminal SG {} too low", terminal_sg);
346 assert!(
347 status.contains("ADEQUATE") || status.contains("GOOD") || status.contains("MARGINAL")
348 );
349 }
350
351 #[test]
352 fn test_stability_parameters_bullet_types() {
353 let match_params = StabilityParameters::for_bullet_type("match", true, false);
354 let vld_params = StabilityParameters::for_bullet_type("vld", true, false);
355 let hunting_params = StabilityParameters::for_bullet_type("hunting", true, true);
356 let default_params = StabilityParameters::for_bullet_type("unknown", false, false);
357
358 assert!(vld_params.nose_shape_factor < match_params.nose_shape_factor);
360
361 assert!(hunting_params.plastic_tip_factor < 1.0);
363
364 assert_eq!(default_params.nose_shape_factor, 1.0);
366 assert_eq!(default_params.boat_tail_factor, 1.0);
367 }
368
369 #[test]
370 fn test_stability_edge_cases() {
371 let zero_twist = calculate_advanced_stability(
373 168.0, 2700.0, 0.0, 0.308, 1.24, 1.225, 288.15, "match", true, false,
374 );
375 assert_eq!(zero_twist, 0.0);
376
377 let zero_caliber = calculate_advanced_stability(
379 168.0, 2700.0, 10.0, 0.0, 1.24, 1.225, 288.15, "match", true, false,
380 );
381 assert_eq!(zero_caliber, 0.0);
382
383 let zero_length = calculate_advanced_stability(
385 168.0, 2700.0, 10.0, 0.308, 0.0, 1.225, 288.15, "match", true, false,
386 );
387 assert_eq!(zero_length, 0.0);
388 }
389
390 #[test]
391 fn test_velocity_correction() {
392 let high_vel = calculate_advanced_stability(
394 168.0, 3000.0, 10.0, 0.308, 1.24, 1.225, 288.15, "match", true, false,
395 );
396 let low_vel = calculate_advanced_stability(
397 168.0, 2000.0, 10.0, 0.308, 1.24, 1.225, 288.15, "match", true, false,
398 );
399
400 assert!(
401 high_vel > low_vel,
402 "Higher velocity ({}) should give higher stability than lower velocity ({})",
403 high_vel,
404 low_vel
405 );
406 }
407
408 #[test]
409 fn test_hypervelocity_correction() {
410 let normal_vel = calculate_advanced_stability(
412 168.0, 2900.0, 10.0, 0.308, 1.24, 1.225, 288.15, "match", true, false,
413 );
414 let hyper_vel = calculate_advanced_stability(
415 168.0, 3500.0, 10.0, 0.308, 1.24, 1.225, 288.15, "match", true, false,
416 );
417
418 assert!(normal_vel > 0.0);
420 assert!(hyper_vel > 0.0);
421 }
422
423 #[test]
424 fn test_atmospheric_correction() {
425 let sea_level = calculate_advanced_stability(
427 168.0, 2700.0, 10.0, 0.308, 1.24, 1.225, 288.15, "match", true, false,
428 );
429 let high_altitude = calculate_advanced_stability(
430 168.0, 2700.0, 10.0, 0.308, 1.24, 1.0, 288.15, "match", true, false,
431 );
432
433 assert!(
434 high_altitude > sea_level,
435 "High altitude ({}) should have higher stability than sea level ({})",
436 high_altitude,
437 sea_level
438 );
439 }
440
441 #[test]
442 fn test_dynamic_stability() {
443 let static_sg = 1.5;
444 let velocity_mps = 800.0;
445 let spin_rate = 1500.0;
446 let caliber_m = 0.00782; let mass_kg = 0.0109; let dynamic_zero_yaw =
451 calculate_dynamic_stability(static_sg, velocity_mps, spin_rate, 0.0, caliber_m, mass_kg);
452
453 let dynamic_with_yaw = calculate_dynamic_stability(
455 static_sg,
456 velocity_mps,
457 spin_rate,
458 0.05, caliber_m,
460 mass_kg,
461 );
462
463 assert!(dynamic_with_yaw < dynamic_zero_yaw);
464 assert!(dynamic_with_yaw > 0.0);
465 }
466
467 #[test]
468 fn test_predict_stability_at_distance() {
469 let initial_sg = 1.8;
470 let initial_vel = 2800.0;
471 let current_vel = 2000.0;
472 let spin_decay = 0.97;
473
474 let predicted = predict_stability_at_distance(initial_sg, initial_vel, current_vel, spin_decay);
475
476 assert!(predicted != initial_sg);
478 assert!(predicted > 0.0);
480 }
481
482 #[test]
483 fn test_predict_stability_edge_cases() {
484 let zero_initial =
486 predict_stability_at_distance(1.5, 0.0, 2000.0, 0.97);
487 assert_eq!(zero_initial, 1.5);
488
489 let zero_current =
491 predict_stability_at_distance(1.5, 2800.0, 0.0, 0.97);
492 assert_eq!(zero_current, 1.5);
493 }
494
495 #[test]
496 fn test_trajectory_stability_status_messages() {
497 let (is_stable, sg, status) = check_trajectory_stability(0.8, 2700.0, 1500.0, 0.95);
499 assert!(!is_stable);
500 assert!(sg < 1.0);
501 assert!(status.contains("UNSTABLE"));
502
503 let (is_stable, sg, status) = check_trajectory_stability(1.4, 2700.0, 2200.0, 0.98);
505 assert!(!is_stable || sg >= 1.0);
506 if sg >= 1.0 && sg < 1.3 {
507 assert!(status.contains("MARGINAL"));
508 }
509
510 let (_, sg, status) = check_trajectory_stability(4.0, 2700.0, 2500.0, 0.99);
512 if sg > 2.5 {
513 assert!(status.contains("OVER-STABILIZED"));
514 }
515 }
516
517 #[test]
518 fn test_different_calibers_stability() {
519 let large_caliber = calculate_advanced_stability(
521 168.0, 2700.0, 10.0, 0.308, 1.24, 1.225, 288.15, "match", true, false,
522 );
523 let small_caliber = calculate_advanced_stability(
524 90.0, 2700.0, 8.0, 0.264, 1.15, 1.225, 288.15, "match", true, false,
525 );
526
527 assert!(large_caliber > 0.0);
529 assert!(small_caliber > 0.0);
530 }
531
532 #[test]
533 fn test_boat_tail_vs_flat_base() {
534 let boat_tail = calculate_advanced_stability(
535 168.0, 2700.0, 10.0, 0.308, 1.24, 1.225, 288.15, "match", true, false,
536 );
537 let flat_base = calculate_advanced_stability(
538 168.0, 2700.0, 10.0, 0.308, 1.24, 1.225, 288.15, "match", false, false,
539 );
540
541 assert!(flat_base > boat_tail);
544 }
545}