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 = apply_atmospheric_correction(
103 sg_velocity_corrected,
104 air_density_kg_m3,
105 temperature_k,
106 );
107
108 let sg_boat_tail = sg_atmosphere_corrected * params.boat_tail_factor;
110
111 let sg_plastic_tip = sg_boat_tail * params.plastic_tip_factor;
113
114 let sg_final = sg_plastic_tip * params.cop_adjustment;
116
117 if velocity_fps > 3000.0 {
119 apply_bowman_howell_correction(sg_final, velocity_fps, caliber_inches)
120 } else {
121 sg_final
122 }
123}
124
125fn calculate_miller_refined(
127 mass_grains: f64,
128 twist_rate_inches: f64,
129 caliber_inches: f64,
130 length_inches: f64,
131 nose_shape_factor: f64,
132) -> f64 {
133 let twist_calibers = twist_rate_inches / caliber_inches;
135 let length_calibers = length_inches / caliber_inches;
136
137 const MILLER_CONSTANT: f64 = 30.0;
139
140 let inertia_factor = 1.0 + length_calibers.powi(2);
143
144 let numerator = MILLER_CONSTANT * mass_grains * nose_shape_factor;
146 let denominator = twist_calibers.powi(2) * caliber_inches.powi(3) *
147 length_calibers * inertia_factor;
148
149 if denominator == 0.0 {
150 return 0.0;
151 }
152
153 numerator / denominator
154}
155
156fn apply_velocity_correction(sg_base: f64, velocity_fps: f64) -> f64 {
158 const VELOCITY_REFERENCE: f64 = 2800.0;
161
162 if velocity_fps < 1400.0 {
164 let velocity_factor = (velocity_fps / 1400.0).powf(0.5);
165 sg_base * velocity_factor * 0.5 } else {
167 let velocity_factor = (velocity_fps / VELOCITY_REFERENCE).powf(1.0 / 3.0);
169 sg_base * velocity_factor
170 }
171}
172
173fn apply_atmospheric_correction(
175 sg: f64,
176 air_density_kg_m3: f64,
177 temperature_k: f64,
178) -> f64 {
179 const STD_DENSITY: f64 = 1.225; const STD_TEMP: f64 = 288.15; let density_ratio = STD_DENSITY / air_density_kg_m3;
185 let density_correction = density_ratio.sqrt();
186
187 let temp_ratio = temperature_k / STD_TEMP;
189 let temp_correction = temp_ratio.powf(0.17); sg * density_correction * temp_correction
192}
193
194fn apply_bowman_howell_correction(sg: f64, velocity_fps: f64, caliber_inches: f64) -> f64 {
196 if velocity_fps <= 3000.0 {
198 return sg;
199 }
200
201 let excess_velocity = (velocity_fps - 3000.0) / 1000.0;
203 let mach_correction = 1.0 - 0.05 * excess_velocity.min(2.0);
204
205 let caliber_factor = if caliber_inches < 0.264 {
207 0.95
208 } else if caliber_inches < 0.308 {
209 0.97
210 } else {
211 1.0
212 };
213
214 sg * mach_correction * caliber_factor
215}
216
217pub fn calculate_dynamic_stability(
219 static_stability: f64,
220 velocity_mps: f64,
221 spin_rate_rad_s: f64,
222 yaw_angle_rad: f64,
223 caliber_m: f64,
224 _mass_kg: f64,
225) -> f64 {
226 let spin_param = if velocity_mps > 0.0 {
230 spin_rate_rad_s * caliber_m / (2.0 * velocity_mps)
231 } else {
232 0.0
233 };
234
235 let yaw_factor = 1.0 - 0.1 * yaw_angle_rad.abs().min(0.1);
237
238 let precession_factor = 1.0 + 0.05 * spin_param.min(0.5);
240
241 static_stability * yaw_factor * precession_factor
242}
243
244pub fn predict_stability_at_distance(
246 initial_stability: f64,
247 initial_velocity_fps: f64,
248 current_velocity_fps: f64,
249 spin_decay_factor: f64, ) -> f64 {
251 if initial_velocity_fps == 0.0 || current_velocity_fps == 0.0 {
252 return initial_stability;
253 }
254
255 let velocity_ratio = current_velocity_fps / initial_velocity_fps;
257
258 let spin_ratio = velocity_ratio * spin_decay_factor;
260
261 let stability_ratio = spin_ratio.powi(2) / velocity_ratio;
264
265 initial_stability * stability_ratio
266}
267
268pub fn check_trajectory_stability(
270 muzzle_stability: f64,
271 muzzle_velocity_fps: f64,
272 terminal_velocity_fps: f64,
273 spin_decay_factor: f64,
274) -> (bool, f64, String) {
275 let terminal_stability = predict_stability_at_distance(
276 muzzle_stability,
277 muzzle_velocity_fps,
278 terminal_velocity_fps,
279 spin_decay_factor,
280 );
281
282 let is_stable = terminal_stability >= 1.3; let status = if terminal_stability < 1.0 {
285 "UNSTABLE - Bullet will tumble".to_string()
286 } else if terminal_stability < 1.3 {
287 "MARGINAL - May experience accuracy issues".to_string()
288 } else if terminal_stability < 1.5 {
289 "ADEQUATE - Acceptable for most conditions".to_string()
290 } else if terminal_stability < 2.5 {
291 "GOOD - Optimal stability".to_string()
292 } else {
293 "OVER-STABILIZED - May reduce BC slightly".to_string()
294 };
295
296 (is_stable, terminal_stability, status)
297}
298
299#[cfg(test)]
300mod tests {
301 use super::*;
302
303 #[test]
304 fn test_advanced_stability() {
305 let stability = calculate_advanced_stability(
307 168.0, 2700.0, 10.0, 0.308, 1.24, 1.225, 288.15, "match", true, false, );
318
319 println!("Calculated stability: {}", stability);
320
321 assert!(stability > 1.3);
323 assert!(stability < 2.5, "Stability {} exceeds upper bound", stability);
324 }
325
326 #[test]
327 fn test_stability_prediction() {
328 let (is_stable, terminal_sg, status) = check_trajectory_stability(
330 2.2, 2700.0, 1900.0, 0.98, );
335
336 println!("is_stable: {}, terminal_sg: {}, status: {}", is_stable, terminal_sg, status);
337
338 assert!(is_stable, "Expected stable trajectory but got: is_stable={}, terminal_sg={}, status={}", is_stable, terminal_sg, status);
339 assert!(terminal_sg > 1.0, "Terminal SG {} too low", terminal_sg);
340 assert!(status.contains("ADEQUATE") || status.contains("GOOD") || status.contains("MARGINAL"));
341 }
342}