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 (MBA-942): canonical Miller is LINEAR in density ratio
177    // (rho0/rho), matching stability.rs and py_ballisticcalc — not sqrt. (This module is
178    // currently dead code, but kept consistent with the canonical correction.)
179    let density_ratio = STD_DENSITY / air_density_kg_m3;
180    let density_correction = density_ratio;
181
182    // Temperature correction (affects speed of sound and viscosity)
183    let temp_ratio = temperature_k / STD_TEMP;
184    let temp_correction = temp_ratio.powf(0.17); // Empirical exponent
185
186    sg * density_correction * temp_correction
187}
188
189/// Bowman-Howell correction for hypervelocity projectiles
190fn apply_bowman_howell_correction(sg: f64, velocity_fps: f64, caliber_inches: f64) -> f64 {
191    // For velocities above 3000 fps, additional dynamic effects occur
192    if velocity_fps <= 3000.0 {
193        return sg;
194    }
195
196    // Hypervelocity correction factor
197    let excess_velocity = (velocity_fps - 3000.0) / 1000.0;
198    let mach_correction = 1.0 - 0.05 * excess_velocity.min(2.0);
199
200    // Small caliber bullets are more affected
201    let caliber_factor = if caliber_inches < 0.264 {
202        0.95
203    } else if caliber_inches < 0.308 {
204        0.97
205    } else {
206        1.0
207    };
208
209    sg * mach_correction * caliber_factor
210}
211
212/// Calculate dynamic stability including yaw effects
213pub fn calculate_dynamic_stability(
214    static_stability: f64,
215    velocity_mps: f64,
216    spin_rate_rad_s: f64,
217    yaw_angle_rad: f64,
218    caliber_m: f64,
219    _mass_kg: f64,
220) -> f64 {
221    // Dynamic stability accounts for yaw and precession
222
223    // Calculate spin parameter
224    let spin_param = if velocity_mps > 0.0 {
225        spin_rate_rad_s * caliber_m / (2.0 * velocity_mps)
226    } else {
227        0.0
228    };
229
230    // Yaw effect on stability
231    let yaw_factor = 1.0 - 0.1 * yaw_angle_rad.abs().min(0.1);
232
233    // Precession damping factor
234    let precession_factor = 1.0 + 0.05 * spin_param.min(0.5);
235
236    static_stability * yaw_factor * precession_factor
237}
238
239/// Predict stability over trajectory with velocity decay
240pub fn predict_stability_at_distance(
241    initial_stability: f64,
242    initial_velocity_fps: f64,
243    current_velocity_fps: f64,
244    spin_decay_factor: f64, // Typically 0.95-0.98
245) -> f64 {
246    if initial_velocity_fps == 0.0 || current_velocity_fps == 0.0 {
247        return initial_stability;
248    }
249
250    // Velocity ratio
251    let velocity_ratio = current_velocity_fps / initial_velocity_fps;
252
253    // Spin decays slower than velocity
254    let spin_ratio = velocity_ratio * spin_decay_factor;
255
256    // Stability changes with velocity and spin
257    // SG ∝ (spin²/velocity)
258    let stability_ratio = spin_ratio.powi(2) / velocity_ratio;
259
260    initial_stability * stability_ratio
261}
262
263/// Check if bullet will remain stable throughout trajectory
264pub fn check_trajectory_stability(
265    muzzle_stability: f64,
266    muzzle_velocity_fps: f64,
267    terminal_velocity_fps: f64,
268    spin_decay_factor: f64,
269) -> (bool, f64, String) {
270    let terminal_stability = predict_stability_at_distance(
271        muzzle_stability,
272        muzzle_velocity_fps,
273        terminal_velocity_fps,
274        spin_decay_factor,
275    );
276
277    let is_stable = terminal_stability >= 1.3; // Minimum for adequate stability
278
279    let status = if terminal_stability < 1.0 {
280        "UNSTABLE - Bullet will tumble".to_string()
281    } else if terminal_stability < 1.3 {
282        "MARGINAL - May experience accuracy issues".to_string()
283    } else if terminal_stability < 1.5 {
284        "ADEQUATE - Acceptable for most conditions".to_string()
285    } else if terminal_stability < 2.5 {
286        "GOOD - Optimal stability".to_string()
287    } else {
288        "OVER-STABILIZED - May reduce BC slightly".to_string()
289    };
290
291    (is_stable, terminal_stability, status)
292}
293
294#[cfg(test)]
295mod tests {
296    use super::*;
297
298    #[test]
299    fn test_advanced_stability() {
300        // Test with .308 168gr Match bullet
301        let stability = calculate_advanced_stability(
302            168.0,   // mass in grains
303            2700.0,  // velocity in fps
304            10.0,    // twist rate in inches
305            0.308,   // caliber in inches
306            1.24,    // length in inches
307            1.225,   // air density
308            288.15,  // temperature in K
309            "match", // bullet type
310            true,    // has boat tail
311            false,   // no plastic tip
312        );
313
314        println!("Calculated stability: {}", stability);
315
316        // Should give stability around 1.4-1.8 for typical .308 Match
317        assert!(stability > 1.3);
318        assert!(
319            stability < 2.5,
320            "Stability {} exceeds upper bound",
321            stability
322        );
323    }
324
325    #[test]
326    fn test_stability_prediction() {
327        // Use higher initial stability to maintain adequate stability through velocity drop
328        let (is_stable, terminal_sg, status) = check_trajectory_stability(
329            2.2,    // muzzle stability (well above marginal)
330            2700.0, // muzzle velocity
331            1900.0, // terminal velocity (moderate drop)
332            0.98,   // spin decay factor (good spin retention)
333        );
334
335        println!(
336            "is_stable: {}, terminal_sg: {}, status: {}",
337            is_stable, terminal_sg, status
338        );
339
340        assert!(
341            is_stable,
342            "Expected stable trajectory but got: is_stable={}, terminal_sg={}, status={}",
343            is_stable, terminal_sg, status
344        );
345        assert!(terminal_sg > 1.0, "Terminal SG {} too low", terminal_sg);
346        assert!(
347            status.contains("ADEQUATE") || status.contains("GOOD") || status.contains("MARGINAL")
348        );
349    }
350
351    #[test]
352    fn test_stability_parameters_bullet_types() {
353        let match_params = StabilityParameters::for_bullet_type("match", true, false);
354        let vld_params = StabilityParameters::for_bullet_type("vld", true, false);
355        let hunting_params = StabilityParameters::for_bullet_type("hunting", true, true);
356        let default_params = StabilityParameters::for_bullet_type("unknown", false, false);
357
358        // VLD should have lower nose_shape_factor (more streamlined)
359        assert!(vld_params.nose_shape_factor < match_params.nose_shape_factor);
360
361        // Hunting with plastic tip should have plastic_tip_factor < 1.0
362        assert!(hunting_params.plastic_tip_factor < 1.0);
363
364        // Default should have all factors at 1.0
365        assert_eq!(default_params.nose_shape_factor, 1.0);
366        assert_eq!(default_params.boat_tail_factor, 1.0);
367    }
368
369    #[test]
370    fn test_stability_edge_cases() {
371        // Zero twist rate should return 0
372        let zero_twist = calculate_advanced_stability(
373            168.0, 2700.0, 0.0, 0.308, 1.24, 1.225, 288.15, "match", true, false,
374        );
375        assert_eq!(zero_twist, 0.0);
376
377        // Zero caliber should return 0
378        let zero_caliber = calculate_advanced_stability(
379            168.0, 2700.0, 10.0, 0.0, 1.24, 1.225, 288.15, "match", true, false,
380        );
381        assert_eq!(zero_caliber, 0.0);
382
383        // Zero length should return 0
384        let zero_length = calculate_advanced_stability(
385            168.0, 2700.0, 10.0, 0.308, 0.0, 1.225, 288.15, "match", true, false,
386        );
387        assert_eq!(zero_length, 0.0);
388    }
389
390    #[test]
391    fn test_velocity_correction() {
392        // Higher velocity should give higher stability
393        let high_vel = calculate_advanced_stability(
394            168.0, 3000.0, 10.0, 0.308, 1.24, 1.225, 288.15, "match", true, false,
395        );
396        let low_vel = calculate_advanced_stability(
397            168.0, 2000.0, 10.0, 0.308, 1.24, 1.225, 288.15, "match", true, false,
398        );
399
400        assert!(
401            high_vel > low_vel,
402            "Higher velocity ({}) should give higher stability than lower velocity ({})",
403            high_vel,
404            low_vel
405        );
406    }
407
408    #[test]
409    fn test_hypervelocity_correction() {
410        // Test Bowman-Howell correction kicks in above 3000 fps
411        let normal_vel = calculate_advanced_stability(
412            168.0, 2900.0, 10.0, 0.308, 1.24, 1.225, 288.15, "match", true, false,
413        );
414        let hyper_vel = calculate_advanced_stability(
415            168.0, 3500.0, 10.0, 0.308, 1.24, 1.225, 288.15, "match", true, false,
416        );
417
418        // Both should be valid (positive) stability values
419        assert!(normal_vel > 0.0);
420        assert!(hyper_vel > 0.0);
421    }
422
423    #[test]
424    fn test_atmospheric_correction() {
425        // Higher altitude (lower density) should increase stability
426        let sea_level = calculate_advanced_stability(
427            168.0, 2700.0, 10.0, 0.308, 1.24, 1.225, 288.15, "match", true, false,
428        );
429        let high_altitude = calculate_advanced_stability(
430            168.0, 2700.0, 10.0, 0.308, 1.24, 1.0, 288.15, "match", true, false,
431        );
432
433        assert!(
434            high_altitude > sea_level,
435            "High altitude ({}) should have higher stability than sea level ({})",
436            high_altitude,
437            sea_level
438        );
439    }
440
441    #[test]
442    fn test_dynamic_stability() {
443        let static_sg = 1.5;
444        let velocity_mps = 800.0;
445        let spin_rate = 1500.0;
446        let caliber_m = 0.00782; // 0.308"
447        let mass_kg = 0.0109; // 168 grains
448
449        // Zero yaw should give stability close to static
450        let dynamic_zero_yaw =
451            calculate_dynamic_stability(static_sg, velocity_mps, spin_rate, 0.0, caliber_m, mass_kg);
452
453        // Some yaw should reduce stability
454        let dynamic_with_yaw = calculate_dynamic_stability(
455            static_sg,
456            velocity_mps,
457            spin_rate,
458            0.05, // ~3 degrees
459            caliber_m,
460            mass_kg,
461        );
462
463        assert!(dynamic_with_yaw < dynamic_zero_yaw);
464        assert!(dynamic_with_yaw > 0.0);
465    }
466
467    #[test]
468    fn test_predict_stability_at_distance() {
469        let initial_sg = 1.8;
470        let initial_vel = 2800.0;
471        let current_vel = 2000.0;
472        let spin_decay = 0.97;
473
474        let predicted = predict_stability_at_distance(initial_sg, initial_vel, current_vel, spin_decay);
475
476        // Should be different from initial
477        assert!(predicted != initial_sg);
478        // Should still be positive
479        assert!(predicted > 0.0);
480    }
481
482    #[test]
483    fn test_predict_stability_edge_cases() {
484        // Zero initial velocity should return initial stability
485        let zero_initial =
486            predict_stability_at_distance(1.5, 0.0, 2000.0, 0.97);
487        assert_eq!(zero_initial, 1.5);
488
489        // Zero current velocity should return initial stability
490        let zero_current =
491            predict_stability_at_distance(1.5, 2800.0, 0.0, 0.97);
492        assert_eq!(zero_current, 1.5);
493    }
494
495    #[test]
496    fn test_trajectory_stability_status_messages() {
497        // Unstable (< 1.0)
498        let (is_stable, sg, status) = check_trajectory_stability(0.8, 2700.0, 1500.0, 0.95);
499        assert!(!is_stable);
500        assert!(sg < 1.0);
501        assert!(status.contains("UNSTABLE"));
502
503        // Marginal (1.0 - 1.3)
504        let (is_stable, sg, status) = check_trajectory_stability(1.4, 2700.0, 2200.0, 0.98);
505        assert!(!is_stable || sg >= 1.0);
506        if sg >= 1.0 && sg < 1.3 {
507            assert!(status.contains("MARGINAL"));
508        }
509
510        // Over-stabilized (> 2.5)
511        let (_, sg, status) = check_trajectory_stability(4.0, 2700.0, 2500.0, 0.99);
512        if sg > 2.5 {
513            assert!(status.contains("OVER-STABILIZED"));
514        }
515    }
516
517    #[test]
518    fn test_different_calibers_stability() {
519        // Smaller caliber with same twist should be less stable
520        let large_caliber = calculate_advanced_stability(
521            168.0, 2700.0, 10.0, 0.308, 1.24, 1.225, 288.15, "match", true, false,
522        );
523        let small_caliber = calculate_advanced_stability(
524            90.0, 2700.0, 8.0, 0.264, 1.15, 1.225, 288.15, "match", true, false,
525        );
526
527        // Both should produce valid stability values
528        assert!(large_caliber > 0.0);
529        assert!(small_caliber > 0.0);
530    }
531
532    #[test]
533    fn test_boat_tail_vs_flat_base() {
534        let boat_tail = calculate_advanced_stability(
535            168.0, 2700.0, 10.0, 0.308, 1.24, 1.225, 288.15, "match", true, false,
536        );
537        let flat_base = calculate_advanced_stability(
538            168.0, 2700.0, 10.0, 0.308, 1.24, 1.225, 288.15, "match", false, false,
539        );
540
541        // Flat base should have slightly higher stability factor applied
542        // (boat_tail_factor < 1.0 for boat tails)
543        assert!(flat_base > boat_tail);
544    }
545}