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 =
103        apply_atmospheric_correction(sg_velocity_corrected, air_density_kg_m3, temperature_k);
104
105    // Apply boat tail correction if applicable
106    let sg_boat_tail = sg_atmosphere_corrected * params.boat_tail_factor;
107
108    // Apply plastic tip correction if applicable
109    let sg_plastic_tip = sg_boat_tail * params.plastic_tip_factor;
110
111    // Apply center of pressure adjustment
112    let sg_final = sg_plastic_tip * params.cop_adjustment;
113
114    // Apply Bowman-Howell dynamic stability correction for very high velocities
115    if velocity_fps > 3000.0 {
116        apply_bowman_howell_correction(sg_final, velocity_fps, caliber_inches)
117    } else {
118        sg_final
119    }
120}
121
122/// Miller's refined stability formula (2005 version)
123fn 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    // Convert to calibers
131    let twist_calibers = twist_rate_inches / caliber_inches;
132    let length_calibers = length_inches / caliber_inches;
133
134    // Miller's constant (refined from original 30)
135    const MILLER_CONSTANT: f64 = 30.0;
136
137    // Calculate moment of inertia factor
138    // For modern bullets: (1 + L²) where L is length in calibers
139    let inertia_factor = 1.0 + length_calibers.powi(2);
140
141    // Base Miller formula with nose shape correction
142    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
153/// Velocity correction using Miller's refined approach
154fn apply_velocity_correction(sg_base: f64, velocity_fps: f64) -> f64 {
155    // Miller's refined velocity correction
156    // Uses 2800 fps as reference with cube root relationship
157    const VELOCITY_REFERENCE: f64 = 2800.0;
158
159    // For velocities below 1400 fps, use modified correction
160    if velocity_fps < 1400.0 {
161        let velocity_factor = (velocity_fps / 1400.0).powf(0.5);
162        sg_base * velocity_factor * 0.5 // Additional reduction for very low velocities
163    } else {
164        // Standard Miller velocity correction
165        let velocity_factor = (velocity_fps / VELOCITY_REFERENCE).powf(1.0 / 3.0);
166        sg_base * velocity_factor
167    }
168}
169
170/// Atmospheric correction for non-standard conditions
171fn apply_atmospheric_correction(sg: f64, air_density_kg_m3: f64, temperature_k: f64) -> f64 {
172    // Standard atmosphere at sea level
173    const STD_DENSITY: f64 = 1.225; // kg/m³
174    const STD_TEMP: f64 = 288.15; // K (15°C)
175
176    // Density altitude correction
177    let density_ratio = STD_DENSITY / air_density_kg_m3;
178    let density_correction = density_ratio.sqrt();
179
180    // Temperature correction (affects speed of sound and viscosity)
181    let temp_ratio = temperature_k / STD_TEMP;
182    let temp_correction = temp_ratio.powf(0.17); // Empirical exponent
183
184    sg * density_correction * temp_correction
185}
186
187/// Bowman-Howell correction for hypervelocity projectiles
188fn apply_bowman_howell_correction(sg: f64, velocity_fps: f64, caliber_inches: f64) -> f64 {
189    // For velocities above 3000 fps, additional dynamic effects occur
190    if velocity_fps <= 3000.0 {
191        return sg;
192    }
193
194    // Hypervelocity correction factor
195    let excess_velocity = (velocity_fps - 3000.0) / 1000.0;
196    let mach_correction = 1.0 - 0.05 * excess_velocity.min(2.0);
197
198    // Small caliber bullets are more affected
199    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
210/// Calculate dynamic stability including yaw effects
211pub 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    // Dynamic stability accounts for yaw and precession
220
221    // Calculate spin parameter
222    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    // Yaw effect on stability
229    let yaw_factor = 1.0 - 0.1 * yaw_angle_rad.abs().min(0.1);
230
231    // Precession damping factor
232    let precession_factor = 1.0 + 0.05 * spin_param.min(0.5);
233
234    static_stability * yaw_factor * precession_factor
235}
236
237/// Predict stability over trajectory with velocity decay
238pub fn predict_stability_at_distance(
239    initial_stability: f64,
240    initial_velocity_fps: f64,
241    current_velocity_fps: f64,
242    spin_decay_factor: f64, // Typically 0.95-0.98
243) -> f64 {
244    if initial_velocity_fps == 0.0 || current_velocity_fps == 0.0 {
245        return initial_stability;
246    }
247
248    // Velocity ratio
249    let velocity_ratio = current_velocity_fps / initial_velocity_fps;
250
251    // Spin decays slower than velocity
252    let spin_ratio = velocity_ratio * spin_decay_factor;
253
254    // Stability changes with velocity and spin
255    // SG ∝ (spin²/velocity)
256    let stability_ratio = spin_ratio.powi(2) / velocity_ratio;
257
258    initial_stability * stability_ratio
259}
260
261/// Check if bullet will remain stable throughout trajectory
262pub 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; // Minimum for adequate stability
276
277    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        // Test with .308 168gr Match bullet
299        let stability = calculate_advanced_stability(
300            168.0,   // mass in grains
301            2700.0,  // velocity in fps
302            10.0,    // twist rate in inches
303            0.308,   // caliber in inches
304            1.24,    // length in inches
305            1.225,   // air density
306            288.15,  // temperature in K
307            "match", // bullet type
308            true,    // has boat tail
309            false,   // no plastic tip
310        );
311
312        println!("Calculated stability: {}", stability);
313
314        // Should give stability around 1.4-1.8 for typical .308 Match
315        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        // Use higher initial stability to maintain adequate stability through velocity drop
326        let (is_stable, terminal_sg, status) = check_trajectory_stability(
327            2.2,    // muzzle stability (well above marginal)
328            2700.0, // muzzle velocity
329            1900.0, // terminal velocity (moderate drop)
330            0.98,   // spin decay factor (good spin retention)
331        );
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}