Skip to main content

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
349    #[test]
350    fn test_stability_parameters_bullet_types() {
351        let match_params = StabilityParameters::for_bullet_type("match", true, false);
352        let vld_params = StabilityParameters::for_bullet_type("vld", true, false);
353        let hunting_params = StabilityParameters::for_bullet_type("hunting", true, true);
354        let default_params = StabilityParameters::for_bullet_type("unknown", false, false);
355
356        // VLD should have lower nose_shape_factor (more streamlined)
357        assert!(vld_params.nose_shape_factor < match_params.nose_shape_factor);
358
359        // Hunting with plastic tip should have plastic_tip_factor < 1.0
360        assert!(hunting_params.plastic_tip_factor < 1.0);
361
362        // Default should have all factors at 1.0
363        assert_eq!(default_params.nose_shape_factor, 1.0);
364        assert_eq!(default_params.boat_tail_factor, 1.0);
365    }
366
367    #[test]
368    fn test_stability_edge_cases() {
369        // Zero twist rate should return 0
370        let zero_twist = calculate_advanced_stability(
371            168.0, 2700.0, 0.0, 0.308, 1.24, 1.225, 288.15, "match", true, false,
372        );
373        assert_eq!(zero_twist, 0.0);
374
375        // Zero caliber should return 0
376        let zero_caliber = calculate_advanced_stability(
377            168.0, 2700.0, 10.0, 0.0, 1.24, 1.225, 288.15, "match", true, false,
378        );
379        assert_eq!(zero_caliber, 0.0);
380
381        // Zero length should return 0
382        let zero_length = calculate_advanced_stability(
383            168.0, 2700.0, 10.0, 0.308, 0.0, 1.225, 288.15, "match", true, false,
384        );
385        assert_eq!(zero_length, 0.0);
386    }
387
388    #[test]
389    fn test_velocity_correction() {
390        // Higher velocity should give higher stability
391        let high_vel = calculate_advanced_stability(
392            168.0, 3000.0, 10.0, 0.308, 1.24, 1.225, 288.15, "match", true, false,
393        );
394        let low_vel = calculate_advanced_stability(
395            168.0, 2000.0, 10.0, 0.308, 1.24, 1.225, 288.15, "match", true, false,
396        );
397
398        assert!(
399            high_vel > low_vel,
400            "Higher velocity ({}) should give higher stability than lower velocity ({})",
401            high_vel,
402            low_vel
403        );
404    }
405
406    #[test]
407    fn test_hypervelocity_correction() {
408        // Test Bowman-Howell correction kicks in above 3000 fps
409        let normal_vel = calculate_advanced_stability(
410            168.0, 2900.0, 10.0, 0.308, 1.24, 1.225, 288.15, "match", true, false,
411        );
412        let hyper_vel = calculate_advanced_stability(
413            168.0, 3500.0, 10.0, 0.308, 1.24, 1.225, 288.15, "match", true, false,
414        );
415
416        // Both should be valid (positive) stability values
417        assert!(normal_vel > 0.0);
418        assert!(hyper_vel > 0.0);
419    }
420
421    #[test]
422    fn test_atmospheric_correction() {
423        // Higher altitude (lower density) should increase stability
424        let sea_level = calculate_advanced_stability(
425            168.0, 2700.0, 10.0, 0.308, 1.24, 1.225, 288.15, "match", true, false,
426        );
427        let high_altitude = calculate_advanced_stability(
428            168.0, 2700.0, 10.0, 0.308, 1.24, 1.0, 288.15, "match", true, false,
429        );
430
431        assert!(
432            high_altitude > sea_level,
433            "High altitude ({}) should have higher stability than sea level ({})",
434            high_altitude,
435            sea_level
436        );
437    }
438
439    #[test]
440    fn test_dynamic_stability() {
441        let static_sg = 1.5;
442        let velocity_mps = 800.0;
443        let spin_rate = 1500.0;
444        let caliber_m = 0.00782; // 0.308"
445        let mass_kg = 0.0109; // 168 grains
446
447        // Zero yaw should give stability close to static
448        let dynamic_zero_yaw =
449            calculate_dynamic_stability(static_sg, velocity_mps, spin_rate, 0.0, caliber_m, mass_kg);
450
451        // Some yaw should reduce stability
452        let dynamic_with_yaw = calculate_dynamic_stability(
453            static_sg,
454            velocity_mps,
455            spin_rate,
456            0.05, // ~3 degrees
457            caliber_m,
458            mass_kg,
459        );
460
461        assert!(dynamic_with_yaw < dynamic_zero_yaw);
462        assert!(dynamic_with_yaw > 0.0);
463    }
464
465    #[test]
466    fn test_predict_stability_at_distance() {
467        let initial_sg = 1.8;
468        let initial_vel = 2800.0;
469        let current_vel = 2000.0;
470        let spin_decay = 0.97;
471
472        let predicted = predict_stability_at_distance(initial_sg, initial_vel, current_vel, spin_decay);
473
474        // Should be different from initial
475        assert!(predicted != initial_sg);
476        // Should still be positive
477        assert!(predicted > 0.0);
478    }
479
480    #[test]
481    fn test_predict_stability_edge_cases() {
482        // Zero initial velocity should return initial stability
483        let zero_initial =
484            predict_stability_at_distance(1.5, 0.0, 2000.0, 0.97);
485        assert_eq!(zero_initial, 1.5);
486
487        // Zero current velocity should return initial stability
488        let zero_current =
489            predict_stability_at_distance(1.5, 2800.0, 0.0, 0.97);
490        assert_eq!(zero_current, 1.5);
491    }
492
493    #[test]
494    fn test_trajectory_stability_status_messages() {
495        // Unstable (< 1.0)
496        let (is_stable, sg, status) = check_trajectory_stability(0.8, 2700.0, 1500.0, 0.95);
497        assert!(!is_stable);
498        assert!(sg < 1.0);
499        assert!(status.contains("UNSTABLE"));
500
501        // Marginal (1.0 - 1.3)
502        let (is_stable, sg, status) = check_trajectory_stability(1.4, 2700.0, 2200.0, 0.98);
503        assert!(!is_stable || sg >= 1.0);
504        if sg >= 1.0 && sg < 1.3 {
505            assert!(status.contains("MARGINAL"));
506        }
507
508        // Over-stabilized (> 2.5)
509        let (_, sg, status) = check_trajectory_stability(4.0, 2700.0, 2500.0, 0.99);
510        if sg > 2.5 {
511            assert!(status.contains("OVER-STABILIZED"));
512        }
513    }
514
515    #[test]
516    fn test_different_calibers_stability() {
517        // Smaller caliber with same twist should be less stable
518        let large_caliber = calculate_advanced_stability(
519            168.0, 2700.0, 10.0, 0.308, 1.24, 1.225, 288.15, "match", true, false,
520        );
521        let small_caliber = calculate_advanced_stability(
522            90.0, 2700.0, 8.0, 0.264, 1.15, 1.225, 288.15, "match", true, false,
523        );
524
525        // Both should produce valid stability values
526        assert!(large_caliber > 0.0);
527        assert!(small_caliber > 0.0);
528    }
529
530    #[test]
531    fn test_boat_tail_vs_flat_base() {
532        let boat_tail = calculate_advanced_stability(
533            168.0, 2700.0, 10.0, 0.308, 1.24, 1.225, 288.15, "match", true, false,
534        );
535        let flat_base = calculate_advanced_stability(
536            168.0, 2700.0, 10.0, 0.308, 1.24, 1.225, 288.15, "match", false, false,
537        );
538
539        // Flat base should have slightly higher stability factor applied
540        // (boat_tail_factor < 1.0 for boat tails)
541        assert!(flat_base > boat_tail);
542    }
543}