ballistics_engine/
spin_drift_advanced.rs

1// Advanced spin drift model based on modern ballistics research
2// Incorporates multiple empirical models from:
3// - Bryan Litz's Applied Ballistics for Long Range Shooting
4// - McCoy's Modern Exterior Ballistics
5// - Courtney & Courtney spin drift research papers
6
7use std::f64::consts::PI;
8
9/// Advanced spin drift coefficients based on extensive field data
10#[derive(Debug, Clone)]
11pub struct SpinDriftCoefficients {
12    /// Litz coefficient for gyroscopic drift (typically 0.8-1.5)
13    pub litz_coefficient: f64,
14    /// McCoy's aerodynamic jump factor
15    pub mccoy_jump_factor: f64,
16    /// Courtney's transonic adjustment
17    pub transonic_factor: f64,
18    /// Yaw damping coefficient
19    pub yaw_damping: f64,
20}
21
22impl SpinDriftCoefficients {
23    /// Get coefficients for specific bullet types based on empirical data
24    pub fn for_bullet_type(bullet_type: &str) -> Self {
25        match bullet_type.to_lowercase().as_str() {
26            "match" | "bthp" | "boat_tail" => Self {
27                litz_coefficient: 1.25,
28                mccoy_jump_factor: 0.85,
29                transonic_factor: 0.75,
30                yaw_damping: 0.92,
31            },
32            "vld" | "very_low_drag" => Self {
33                litz_coefficient: 1.15,
34                mccoy_jump_factor: 0.78,
35                transonic_factor: 0.68,
36                yaw_damping: 0.88,
37            },
38            "hybrid" | "hybrid_ogive" => Self {
39                litz_coefficient: 1.20,
40                mccoy_jump_factor: 0.82,
41                transonic_factor: 0.72,
42                yaw_damping: 0.90,
43            },
44            "flat_base" | "fb" => Self {
45                litz_coefficient: 1.35,
46                mccoy_jump_factor: 0.95,
47                transonic_factor: 0.85,
48                yaw_damping: 0.95,
49            },
50            _ => Self::default(),
51        }
52    }
53
54    pub fn default() -> Self {
55        Self {
56            litz_coefficient: 1.25,
57            mccoy_jump_factor: 0.85,
58            transonic_factor: 0.75,
59            yaw_damping: 0.92,
60        }
61    }
62}
63
64/// Calculate advanced spin drift using multiple empirical models
65pub fn calculate_advanced_spin_drift(
66    stability_factor: f64,
67    time_of_flight_s: f64,
68    velocity_mps: f64,
69    muzzle_velocity_mps: f64,
70    spin_rate_rad_s: f64,
71    caliber_m: f64,
72    mass_kg: f64,
73    air_density_kg_m3: f64,
74    is_right_twist: bool,
75    bullet_type: &str,
76) -> f64 {
77    // Edge cases: no drift if no time or no stability
78    // MBA-205: Guard against division by zero
79    if time_of_flight_s <= 0.0 || stability_factor <= 0.0
80        || muzzle_velocity_mps <= 0.0 || air_density_kg_m3 <= 0.0 {
81        return 0.0;
82    }
83    
84    let coeffs = SpinDriftCoefficients::for_bullet_type(bullet_type);
85    
86    // Direction based on twist
87    let sign = if is_right_twist { 1.0 } else { -1.0 };
88    
89    // Calculate Mach numbers
90    let mach_current = velocity_mps / 343.0;
91    let mach_muzzle = muzzle_velocity_mps / 343.0;
92    
93    // 1. Litz's empirical formula (primary component)
94    let litz_drift = calculate_litz_drift(
95        stability_factor,
96        time_of_flight_s,
97        coeffs.litz_coefficient,
98    );
99    
100    // 2. McCoy's aerodynamic jump correction
101    let jump_correction = calculate_aerodynamic_jump_correction(
102        mach_muzzle,
103        spin_rate_rad_s,
104        caliber_m,
105        mass_kg,
106        coeffs.mccoy_jump_factor,
107    );
108    
109    // 3. Transonic correction factor
110    let transonic_correction = calculate_transonic_correction(
111        mach_current,
112        coeffs.transonic_factor,
113    );
114    
115    // 4. Yaw damping effect
116    let yaw_factor = calculate_yaw_damping_factor(
117        stability_factor,
118        time_of_flight_s,
119        coeffs.yaw_damping,
120    );
121    
122    // 5. Velocity decay correction (new research)
123    let velocity_ratio = velocity_mps / muzzle_velocity_mps;
124    let velocity_correction = velocity_ratio.powf(0.3);
125    
126    // Combine all components
127    let total_drift = sign * (
128        litz_drift * transonic_correction * yaw_factor * velocity_correction
129        + jump_correction
130    );
131    
132    // Apply atmospheric density correction
133    let density_correction = (1.225 / air_density_kg_m3).sqrt();
134    
135    total_drift * density_correction
136}
137
138/// Litz's empirical formula with refined coefficients
139fn calculate_litz_drift(stability: f64, time_s: f64, coefficient: f64) -> f64 {
140    if stability <= 1.0 || time_s <= 0.0 {
141        return 0.0;
142    }
143    
144    // Refined Litz formula based on extensive field testing
145    // SD = k * (SG + 1.2) * TOF^1.83
146    // where k is empirically determined for bullet type
147    let base_drift = coefficient * (stability + 1.2) * time_s.powf(1.83);
148    
149    // Convert inches to meters
150    base_drift * 0.0254
151}
152
153/// McCoy's aerodynamic jump correction at muzzle exit
154fn calculate_aerodynamic_jump_correction(
155    mach: f64,
156    spin_rate_rad_s: f64,
157    caliber_m: f64,
158    mass_kg: f64,
159    jump_factor: f64,
160) -> f64 {
161    // Aerodynamic jump contributes to initial displacement
162    // Based on McCoy's research on muzzle exit effects
163
164    // MBA-205: Guard against division by zero when mach == 0
165    if mach <= 0.0 {
166        return 0.0;
167    }
168
169    let spin_parameter = spin_rate_rad_s * caliber_m / (2.0 * 343.0 * mach);
170    
171    // Jump magnitude in milliradians
172    let jump_mrad = jump_factor * spin_parameter * (mass_kg / 0.01).sqrt();
173    
174    // Convert to lateral displacement (approximation for small angles)
175    // This is a one-time displacement, not time-dependent
176    jump_mrad * 0.001 * 100.0 // Approximate 100m reference distance
177}
178
179/// Transonic correction based on Courtney & Courtney research
180fn calculate_transonic_correction(mach: f64, transonic_factor: f64) -> f64 {
181    if mach < 0.8 {
182        // Subsonic - minimal correction needed
183        1.0
184    } else if mach < 1.2 {
185        // Transonic region - significant instability
186        // Smooth transition using cosine interpolation
187        let transonic_ratio = (mach - 0.8) / 0.4;
188        let base_correction = 1.0 + (transonic_factor - 1.0) * (1.0 - (transonic_ratio * PI).cos()) / 2.0;
189        base_correction
190    } else {
191        // Supersonic - stable again but reduced effect
192        0.85 + 0.15 * (2.5 - mach).max(0.0) / 1.3
193    }
194}
195
196/// Yaw damping factor based on stability and time
197fn calculate_yaw_damping_factor(stability: f64, time_s: f64, damping_coeff: f64) -> f64 {
198    // Yaw oscillations damp out over time
199    // Higher stability = faster damping
200    let damping_rate = damping_coeff * stability.sqrt();
201    let damped = 1.0 - (-damping_rate * time_s).exp();
202    
203    // Ensure reasonable bounds
204    damped.max(0.5).min(1.0)
205}
206
207/// Calculate equilibrium yaw angle using advanced model
208pub fn calculate_advanced_yaw_of_repose(
209    stability_factor: f64,
210    velocity_mps: f64,
211    crosswind_mps: f64,
212    spin_rate_rad_s: f64,
213    air_density_kg_m3: f64,
214    caliber_m: f64,
215) -> f64 {
216    if stability_factor <= 1.0 || velocity_mps <= 0.0 {
217        return 0.0;
218    }
219    
220    // Base yaw from crosswind
221    let wind_yaw = if crosswind_mps != 0.0 && velocity_mps > 0.0 {
222        (crosswind_mps / velocity_mps).atan()
223    } else {
224        // Natural yaw from trajectory curvature (gravity-induced)
225        // Empirical value based on typical trajectories
226        0.001 + 0.0005 * (velocity_mps / 800.0).min(2.0)
227    };
228    
229    // Stability-based damping (McCoy's model)
230    let stability_term = ((stability_factor - 1.0) / stability_factor).sqrt();
231    
232    // Dynamic pressure effect
233    let q = 0.5 * air_density_kg_m3 * velocity_mps.powi(2);
234    let q_factor = (q / 50000.0).min(1.5).max(0.5); // Normalize around typical q
235    
236    // Spin effect on yaw response
237    let spin_factor = if spin_rate_rad_s > 0.0 {
238        let spin_param = spin_rate_rad_s * caliber_m / (2.0 * velocity_mps);
239        1.0 + 0.2 * spin_param.min(0.5)
240    } else {
241        1.0
242    };
243    
244    wind_yaw * stability_term * q_factor * spin_factor
245}
246
247/// Data-driven correction factor (placeholder for ML integration)
248pub fn apply_ml_correction(
249    base_drift: f64,
250    stability: f64,
251    mach: f64,
252    time_s: f64,
253    caliber_inches: f64,
254    mass_grains: f64,
255) -> f64 {
256    // This function would integrate with ML models trained on real-world data
257    // For now, returns the base drift unmodified
258    // 
259    // In production, this would:
260    // 1. Extract features: [stability, mach, time_s, caliber_inches, mass_grains]
261    // 2. Pass to trained neural network or gradient boosting model
262    // 3. Return correction factor (typically 0.8-1.2)
263    // 4. Multiply base_drift by correction factor
264    
265    // Placeholder implementation with simple heuristics
266    let mut correction = 1.0;
267    
268    // Known adjustments from field data
269    if stability > 2.5 && mach < 1.0 {
270        correction *= 0.92; // Over-stabilized subsonic tends to drift less
271    }
272    
273    if time_s > 2.0 && mach < 0.9 {
274        correction *= 1.08; // Long flight subsonic needs more correction
275    }
276    
277    if caliber_inches < 0.264 && mass_grains < 100.0 {
278        correction *= 0.88; // Light, small caliber bullets drift less
279    }
280    
281    base_drift * correction
282}
283
284#[cfg(test)]
285mod tests {
286    use super::*;
287
288    #[test]
289    fn test_advanced_spin_drift() {
290        // Test with typical .308 Match bullet
291        let drift = calculate_advanced_spin_drift(
292            1.5,        // stability
293            1.2,        // time of flight
294            600.0,      // current velocity m/s
295            850.0,      // muzzle velocity m/s
296            1500.0,     // spin rate rad/s
297            0.00308,    // caliber in meters
298            0.0108,     // mass in kg (168 grains)
299            1.225,      // air density
300            true,       // right twist
301            "match",    // bullet type
302        );
303        
304        // Should give reasonable drift (2-8 inches at 1000 yards typical)
305        assert!(drift > 0.0);
306        assert!(drift < 0.3); // Less than 12 inches in meters
307    }
308
309    #[test]
310    fn test_transonic_correction() {
311        let subsonic = calculate_transonic_correction(0.7, 0.75);
312        let transonic = calculate_transonic_correction(1.0, 0.75);
313        let supersonic = calculate_transonic_correction(1.5, 0.75);
314        
315        assert_eq!(subsonic, 1.0);
316        assert!(transonic > 0.8 && transonic < 1.0);
317        assert!(supersonic > 0.7 && supersonic < 1.0);
318    }
319}