ballistics_engine/
stability_advanced.rs

1// Advanced stability calculations using refined Miller formula and modern corrections
2// Based on:
3// - Don Miller's refined stability formula (2005)
4// - Bryan Litz's stability refinements
5// - Geoffrey Kolbe's Bowman-Howell stability improvements
6//
7// NOTE: Some advanced stability functions are experimental and kept for future use.
8#![allow(dead_code)]
9
10/// Advanced stability parameters for different bullet types
11#[derive(Debug, Clone)]
12pub struct StabilityParameters {
13    /// Shape factor for nose profile (1.0 for tangent, 0.9 for secant)
14    pub nose_shape_factor: f64,
15    /// Boat tail effectiveness factor
16    pub boat_tail_factor: f64,
17    /// Plastic tip correction
18    pub plastic_tip_factor: f64,
19    /// Center of pressure adjustment
20    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
70/// Calculate advanced Miller stability with modern corrections
71pub 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    // Calculate base Miller stability
90    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    // Apply velocity correction (Miller's refined formula)
99    let sg_velocity_corrected = apply_velocity_correction(sg_base, velocity_fps);
100    
101    // Apply atmospheric corrections
102    let sg_atmosphere_corrected = apply_atmospheric_correction(
103        sg_velocity_corrected,
104        air_density_kg_m3,
105        temperature_k,
106    );
107    
108    // Apply boat tail correction if applicable
109    let sg_boat_tail = sg_atmosphere_corrected * params.boat_tail_factor;
110    
111    // Apply plastic tip correction if applicable
112    let sg_plastic_tip = sg_boat_tail * params.plastic_tip_factor;
113    
114    // Apply center of pressure adjustment
115    let sg_final = sg_plastic_tip * params.cop_adjustment;
116    
117    // Apply Bowman-Howell dynamic stability correction for very high velocities
118    if velocity_fps > 3000.0 {
119        apply_bowman_howell_correction(sg_final, velocity_fps, caliber_inches)
120    } else {
121        sg_final
122    }
123}
124
125/// Miller's refined stability formula (2005 version)
126fn 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    // Convert to calibers
134    let twist_calibers = twist_rate_inches / caliber_inches;
135    let length_calibers = length_inches / caliber_inches;
136    
137    // Miller's constant (refined from original 30)
138    const MILLER_CONSTANT: f64 = 30.0;
139    
140    // Calculate moment of inertia factor
141    // For modern bullets: (1 + L²) where L is length in calibers
142    let inertia_factor = 1.0 + length_calibers.powi(2);
143    
144    // Base Miller formula with nose shape correction
145    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
156/// Velocity correction using Miller's refined approach
157fn apply_velocity_correction(sg_base: f64, velocity_fps: f64) -> f64 {
158    // Miller's refined velocity correction
159    // Uses 2800 fps as reference with cube root relationship
160    const VELOCITY_REFERENCE: f64 = 2800.0;
161    
162    // For velocities below 1400 fps, use modified correction
163    if velocity_fps < 1400.0 {
164        let velocity_factor = (velocity_fps / 1400.0).powf(0.5);
165        sg_base * velocity_factor * 0.5 // Additional reduction for very low velocities
166    } else {
167        // Standard Miller velocity correction
168        let velocity_factor = (velocity_fps / VELOCITY_REFERENCE).powf(1.0 / 3.0);
169        sg_base * velocity_factor
170    }
171}
172
173/// Atmospheric correction for non-standard conditions
174fn apply_atmospheric_correction(
175    sg: f64,
176    air_density_kg_m3: f64,
177    temperature_k: f64,
178) -> f64 {
179    // Standard atmosphere at sea level
180    const STD_DENSITY: f64 = 1.225; // kg/m³
181    const STD_TEMP: f64 = 288.15;   // K (15°C)
182    
183    // Density altitude correction
184    let density_ratio = STD_DENSITY / air_density_kg_m3;
185    let density_correction = density_ratio.sqrt();
186    
187    // Temperature correction (affects speed of sound and viscosity)
188    let temp_ratio = temperature_k / STD_TEMP;
189    let temp_correction = temp_ratio.powf(0.17); // Empirical exponent
190    
191    sg * density_correction * temp_correction
192}
193
194/// Bowman-Howell correction for hypervelocity projectiles
195fn apply_bowman_howell_correction(sg: f64, velocity_fps: f64, caliber_inches: f64) -> f64 {
196    // For velocities above 3000 fps, additional dynamic effects occur
197    if velocity_fps <= 3000.0 {
198        return sg;
199    }
200    
201    // Hypervelocity correction factor
202    let excess_velocity = (velocity_fps - 3000.0) / 1000.0;
203    let mach_correction = 1.0 - 0.05 * excess_velocity.min(2.0);
204    
205    // Small caliber bullets are more affected
206    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
217/// Calculate dynamic stability including yaw effects
218pub 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    // Dynamic stability accounts for yaw and precession
227    
228    // Calculate spin parameter
229    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    // Yaw effect on stability
236    let yaw_factor = 1.0 - 0.1 * yaw_angle_rad.abs().min(0.1);
237    
238    // Precession damping factor
239    let precession_factor = 1.0 + 0.05 * spin_param.min(0.5);
240    
241    static_stability * yaw_factor * precession_factor
242}
243
244/// Predict stability over trajectory with velocity decay
245pub fn predict_stability_at_distance(
246    initial_stability: f64,
247    initial_velocity_fps: f64,
248    current_velocity_fps: f64,
249    spin_decay_factor: f64, // Typically 0.95-0.98
250) -> f64 {
251    if initial_velocity_fps == 0.0 || current_velocity_fps == 0.0 {
252        return initial_stability;
253    }
254    
255    // Velocity ratio
256    let velocity_ratio = current_velocity_fps / initial_velocity_fps;
257    
258    // Spin decays slower than velocity
259    let spin_ratio = velocity_ratio * spin_decay_factor;
260    
261    // Stability changes with velocity and spin
262    // SG ∝ (spin²/velocity)
263    let stability_ratio = spin_ratio.powi(2) / velocity_ratio;
264    
265    initial_stability * stability_ratio
266}
267
268/// Check if bullet will remain stable throughout trajectory
269pub 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; // Minimum for adequate stability
283    
284    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        // Test with .308 168gr Match bullet
306        let stability = calculate_advanced_stability(
307            168.0,      // mass in grains
308            2700.0,     // velocity in fps
309            10.0,       // twist rate in inches
310            0.308,      // caliber in inches
311            1.24,       // length in inches
312            1.225,      // air density
313            288.15,     // temperature in K
314            "match",    // bullet type
315            true,       // has boat tail
316            false,      // no plastic tip
317        );
318
319        println!("Calculated stability: {}", stability);
320
321        // Should give stability around 1.4-1.8 for typical .308 Match
322        assert!(stability > 1.3);
323        assert!(stability < 2.5, "Stability {} exceeds upper bound", stability);
324    }
325
326    #[test]
327    fn test_stability_prediction() {
328        // Use higher initial stability to maintain adequate stability through velocity drop
329        let (is_stable, terminal_sg, status) = check_trajectory_stability(
330            2.2,        // muzzle stability (well above marginal)
331            2700.0,     // muzzle velocity
332            1900.0,     // terminal velocity (moderate drop)
333            0.98,       // spin decay factor (good spin retention)
334        );
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}