ballistics_engine/
stability.rs

1use crate::InternalBallisticInputs as BallisticInputs;
2
3/// Calculate the gyroscopic stability coefficient (SG) for the bullet.
4///
5/// This function uses the Miller stability formula. An SG value greater than 1.5
6/// is generally considered to indicate adequate stability.
7///
8/// # Arguments
9/// * `inputs` - Ballistic input parameters
10/// * `atmo_params` - Atmospheric parameters (altitude, temp_c, pressure_hpa, density_ratio)
11///
12/// # Returns
13/// * Stability coefficient (dimensionless)
14pub fn compute_stability_coefficient(
15    inputs: &BallisticInputs,
16    atmo_params: (f64, f64, f64, f64),
17) -> f64 {
18    // Check for required parameters
19    if inputs.twist_rate == 0.0 || inputs.bullet_length == 0.0 || inputs.bullet_diameter == 0.0 {
20        return 0.0;
21    }
22
23    // Pre-calculated constants for efficiency
24    const MILLER_CONST: f64 = 30.0;
25    const VEL_REF_FPS: f64 = 2800.0;
26    const TEMP_REF_K: f64 = 288.15; // 15°C
27    const PRESS_REF_HPA: f64 = 1013.25;
28
29    // Calculate intermediate values
30    // Convert twist rate from inches to meters for consistency
31    let twist_rate_m = inputs.twist_rate.abs() * 0.0254; // inches to meters
32    let twist_calibers = twist_rate_m / inputs.bullet_diameter;
33    let length_calibers = inputs.bullet_length / inputs.bullet_diameter;
34
35    // Convert units for Miller formula
36    let mass_grains = inputs.bullet_mass / 0.00006479891; // kg to grains
37    let diameter_inches = inputs.bullet_diameter / 0.0254; // meters to inches
38    let velocity_fps = inputs.muzzle_velocity * 3.28084; // m/s to fps
39
40    // Miller stability formula components
41    let mass_term = MILLER_CONST * mass_grains;
42    let geom_term = twist_calibers.powi(2)
43        * diameter_inches.powi(3)
44        * length_calibers
45        * (1.0 + length_calibers.powi(2));
46
47    if geom_term == 0.0 {
48        return 0.0;
49    }
50
51    // Extract atmospheric parameters
52    let (_, temp_c, current_press_hpa, _) = atmo_params;
53    let temp_k = temp_c + 273.15;
54
55    // Atmospheric density correction factor
56    // Ratio of reference density to current density
57    let density_correction = (temp_k / TEMP_REF_K) * (PRESS_REF_HPA / current_press_hpa);
58
59    // Velocity correction factor
60    let velocity_correction = (velocity_fps / VEL_REF_FPS).powf(1.0 / 3.0);
61
62    // Final stability calculation
63
64    (mass_term / geom_term) * velocity_correction * density_correction
65}
66
67/// Calculate spin drift in meters using Litz approximation.
68///
69/// # Arguments
70/// * `time_s` - Time of flight in seconds
71/// * `stability` - Stability coefficient
72/// * `twist_rate` - Twist rate in inches (calibers per turn)
73/// * `is_twist_right` - True for right-hand twist, false for left-hand
74///
75/// # Returns
76/// * Spin drift in meters
77pub fn compute_spin_drift(
78    time_s: f64,
79    stability: f64,
80    twist_rate: f64,
81    is_twist_right: bool,
82) -> f64 {
83    compute_spin_drift_with_decay(time_s, stability, twist_rate, is_twist_right, None)
84}
85
86/// Calculate spin drift with optional spin decay modeling
87pub fn compute_spin_drift_with_decay(
88    time_s: f64,
89    stability: f64,
90    twist_rate: f64,
91    is_twist_right: bool,
92    decay_factor: Option<f64>, // Optional spin decay factor (0-1)
93) -> f64 {
94    if stability == 0.0 || time_s <= 0.0 || twist_rate == 0.0 {
95        return 0.0;
96    }
97
98    let sign = if is_twist_right { 1.0 } else { -1.0 };
99
100    // Modified formula with more realistic scaling
101    // Original Litz: SD = 1.25 * (SG + 1.2) * TOF^1.83
102    // This overestimates significantly for short TOF
103    // Using a modified version with better scaling factor
104    let scaling_factor = 0.075; // Reduced from 1.25 to give realistic values
105    let base_drift = sign * scaling_factor * (stability + 1.2) * time_s.powf(1.83);
106
107    // Apply spin decay if provided
108    let effective_drift = if let Some(decay) = decay_factor {
109        base_drift * decay.max(0.0).min(1.0)
110    } else {
111        base_drift
112    };
113
114    // Convert inches to meters
115    effective_drift * 0.0254
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    fn create_test_inputs() -> BallisticInputs {
123        BallisticInputs {
124            muzzle_velocity: 823.0, // 2700 fps in m/s
125            bc_value: 0.5,
126            bullet_mass: 0.0109,      // 168 grains in kg
127            bullet_diameter: 0.00782, // 0.308 inches in meters
128            bullet_length: 0.033,     // in meters (1.3 inches)
129            twist_rate: 10.0,
130            ..Default::default()
131        }
132    }
133
134    #[test]
135    fn test_compute_stability_coefficient() {
136        let inputs = create_test_inputs();
137        let atmo_params = (0.0, 15.0, 1013.25, 1.0); // Standard conditions
138
139        let stability = compute_stability_coefficient(&inputs, atmo_params);
140
141        // Debug output
142        println!("Computed stability: {}", stability);
143
144        // Should be a reasonable stability value
145        assert!(stability > 0.0);
146        assert!(stability < 10.0); // Sanity check
147
148        // Test with typical values should give stability around 1.5-2.5
149        assert!(stability > 1.0);
150        assert!(stability < 3.0);
151    }
152
153    #[test]
154    fn test_compute_stability_coefficient_zero_cases() {
155        let mut inputs = create_test_inputs();
156        let atmo_params = (0.0, 15.0, 1013.25, 1.0);
157
158        // Test with zero twist rate
159        inputs.twist_rate = 0.0;
160        assert_eq!(compute_stability_coefficient(&inputs, atmo_params), 0.0);
161
162        // Test with zero bullet length
163        inputs = create_test_inputs();
164        inputs.bullet_length = 0.0;
165        assert_eq!(compute_stability_coefficient(&inputs, atmo_params), 0.0);
166
167        // Test with zero bullet diameter
168        inputs = create_test_inputs();
169        inputs.bullet_diameter = 0.0;
170        assert_eq!(compute_stability_coefficient(&inputs, atmo_params), 0.0);
171    }
172
173    #[test]
174    fn test_compute_stability_coefficient_atmospheric_effects() {
175        let inputs = create_test_inputs();
176
177        // Standard conditions
178        let standard_atmo = (0.0, 15.0, 1013.25, 1.0);
179        let standard_stability = compute_stability_coefficient(&inputs, standard_atmo);
180
181        // High altitude (lower pressure, lower temperature)
182        let high_alt_atmo = (3000.0, 5.0, 900.0, 1.0);
183        let high_alt_stability = compute_stability_coefficient(&inputs, high_alt_atmo);
184
185        // High altitude should have higher stability due to lower air density
186        assert!(high_alt_stability > standard_stability);
187
188        // Hot conditions (higher temperature)
189        let hot_atmo = (0.0, 35.0, 1013.25, 1.0);
190        let hot_stability = compute_stability_coefficient(&inputs, hot_atmo);
191
192        // Hot conditions should have higher stability due to lower air density
193        assert!(hot_stability > standard_stability);
194    }
195
196    #[test]
197    fn test_compute_spin_drift() {
198        let time_s = 1.5;
199        let stability = 2.0;
200        let twist_rate = 10.0;
201
202        // Test right-hand twist
203        let drift_right = compute_spin_drift(time_s, stability, twist_rate, true);
204        assert!(drift_right > 0.0); // Should drift to the right (positive)
205
206        // Test left-hand twist
207        let drift_left = compute_spin_drift(time_s, stability, twist_rate, false);
208        assert!(drift_left < 0.0); // Should drift to the left (negative)
209        assert!((drift_left + drift_right).abs() < 1e-10); // Should be equal magnitude
210
211        // Test reasonable magnitude (should be small)
212        assert!(drift_right.abs() < 0.1); // Less than 10cm for 1.5s flight
213    }
214
215    #[test]
216    fn test_compute_spin_drift_zero_cases() {
217        // Test with zero stability
218        assert_eq!(compute_spin_drift(1.5, 0.0, 10.0, true), 0.0);
219
220        // Test with zero time
221        assert_eq!(compute_spin_drift(0.0, 2.0, 10.0, true), 0.0);
222
223        // Test with negative time
224        assert_eq!(compute_spin_drift(-1.0, 2.0, 10.0, true), 0.0);
225
226        // Test with zero twist rate
227        assert_eq!(compute_spin_drift(1.5, 2.0, 0.0, true), 0.0);
228    }
229
230    #[test]
231    fn test_compute_spin_drift_scaling() {
232        let stability = 2.0;
233        let twist_rate = 10.0;
234
235        // Test time scaling
236        let drift_1s = compute_spin_drift(1.0, stability, twist_rate, true);
237        let drift_2s = compute_spin_drift(2.0, stability, twist_rate, true);
238
239        // Drift should increase with time (non-linearly due to 1.83 exponent)
240        assert!(drift_2s > drift_1s);
241
242        // Test stability scaling
243        let drift_low_stability = compute_spin_drift(1.5, 1.0, twist_rate, true);
244        let drift_high_stability = compute_spin_drift(1.5, 3.0, twist_rate, true);
245
246        // Higher stability should produce more drift
247        assert!(drift_high_stability > drift_low_stability);
248    }
249}