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;
178 let density_correction = density_ratio.sqrt();
179
180 let temp_ratio = temperature_k / STD_TEMP;
182 let temp_correction = temp_ratio.powf(0.17); sg * density_correction * temp_correction
185}
186
187fn apply_bowman_howell_correction(sg: f64, velocity_fps: f64, caliber_inches: f64) -> f64 {
189 if velocity_fps <= 3000.0 {
191 return sg;
192 }
193
194 let excess_velocity = (velocity_fps - 3000.0) / 1000.0;
196 let mach_correction = 1.0 - 0.05 * excess_velocity.min(2.0);
197
198 let caliber_factor = if caliber_inches < 0.264 {
200 0.95
201 } else if caliber_inches < 0.308 {
202 0.97
203 } else {
204 1.0
205 };
206
207 sg * mach_correction * caliber_factor
208}
209
210pub fn calculate_dynamic_stability(
212 static_stability: f64,
213 velocity_mps: f64,
214 spin_rate_rad_s: f64,
215 yaw_angle_rad: f64,
216 caliber_m: f64,
217 _mass_kg: f64,
218) -> f64 {
219 let spin_param = if velocity_mps > 0.0 {
223 spin_rate_rad_s * caliber_m / (2.0 * velocity_mps)
224 } else {
225 0.0
226 };
227
228 let yaw_factor = 1.0 - 0.1 * yaw_angle_rad.abs().min(0.1);
230
231 let precession_factor = 1.0 + 0.05 * spin_param.min(0.5);
233
234 static_stability * yaw_factor * precession_factor
235}
236
237pub fn predict_stability_at_distance(
239 initial_stability: f64,
240 initial_velocity_fps: f64,
241 current_velocity_fps: f64,
242 spin_decay_factor: f64, ) -> f64 {
244 if initial_velocity_fps == 0.0 || current_velocity_fps == 0.0 {
245 return initial_stability;
246 }
247
248 let velocity_ratio = current_velocity_fps / initial_velocity_fps;
250
251 let spin_ratio = velocity_ratio * spin_decay_factor;
253
254 let stability_ratio = spin_ratio.powi(2) / velocity_ratio;
257
258 initial_stability * stability_ratio
259}
260
261pub fn check_trajectory_stability(
263 muzzle_stability: f64,
264 muzzle_velocity_fps: f64,
265 terminal_velocity_fps: f64,
266 spin_decay_factor: f64,
267) -> (bool, f64, String) {
268 let terminal_stability = predict_stability_at_distance(
269 muzzle_stability,
270 muzzle_velocity_fps,
271 terminal_velocity_fps,
272 spin_decay_factor,
273 );
274
275 let is_stable = terminal_stability >= 1.3; let status = if terminal_stability < 1.0 {
278 "UNSTABLE - Bullet will tumble".to_string()
279 } else if terminal_stability < 1.3 {
280 "MARGINAL - May experience accuracy issues".to_string()
281 } else if terminal_stability < 1.5 {
282 "ADEQUATE - Acceptable for most conditions".to_string()
283 } else if terminal_stability < 2.5 {
284 "GOOD - Optimal stability".to_string()
285 } else {
286 "OVER-STABILIZED - May reduce BC slightly".to_string()
287 };
288
289 (is_stable, terminal_stability, status)
290}
291
292#[cfg(test)]
293mod tests {
294 use super::*;
295
296 #[test]
297 fn test_advanced_stability() {
298 let stability = calculate_advanced_stability(
300 168.0, 2700.0, 10.0, 0.308, 1.24, 1.225, 288.15, "match", true, false, );
311
312 println!("Calculated stability: {}", stability);
313
314 assert!(stability > 1.3);
316 assert!(
317 stability < 2.5,
318 "Stability {} exceeds upper bound",
319 stability
320 );
321 }
322
323 #[test]
324 fn test_stability_prediction() {
325 let (is_stable, terminal_sg, status) = check_trajectory_stability(
327 2.2, 2700.0, 1900.0, 0.98, );
332
333 println!(
334 "is_stable: {}, terminal_sg: {}, status: {}",
335 is_stable, terminal_sg, status
336 );
337
338 assert!(
339 is_stable,
340 "Expected stable trajectory but got: is_stable={}, terminal_sg={}, status={}",
341 is_stable, terminal_sg, status
342 );
343 assert!(terminal_sg > 1.0, "Terminal SG {} too low", terminal_sg);
344 assert!(
345 status.contains("ADEQUATE") || status.contains("GOOD") || status.contains("MARGINAL")
346 );
347 }
348}