ballistics_engine/
spin_drift.rs

1use std::f64::consts::PI;
2use crate::spin_decay::{update_spin_rate, SpinDecayParameters};
3use crate::pitch_damping::{
4    calculate_damped_yaw_of_repose, PitchDampingCoefficients,
5    calculate_pitch_damping_moment,
6};
7
8/// Components of enhanced spin drift calculation
9#[derive(Debug, Clone)]
10pub struct SpinDriftComponents {
11    pub spin_rate_rps: f64,        // Revolutions per second
12    pub spin_rate_rad_s: f64,      // Radians per second
13    pub stability_factor: f64,     // Gyroscopic stability (Sg)
14    pub yaw_of_repose_rad: f64,    // Equilibrium yaw angle
15    pub drift_rate_mps: f64,       // Lateral drift rate (m/s)
16    pub total_drift_m: f64,        // Total drift at current time
17    pub magnus_component_m: f64,   // Magnus effect contribution
18    pub gyroscopic_component_m: f64, // Pure gyroscopic drift
19    pub pitch_damping_moment: f64, // Pitch damping moment (N⋅m)
20    pub yaw_convergence_rate: f64, // Convergence rate to equilibrium (rad/s)
21    pub pitch_rate_rad_s: f64,     // Current pitch/yaw rate (rad/s)
22}
23
24/// Calculate bullet spin rate from velocity and twist rate
25pub fn calculate_spin_rate(velocity_mps: f64, twist_rate_inches: f64) -> (f64, f64) {
26    if twist_rate_inches <= 0.0 {
27        return (0.0, 0.0);
28    }
29    
30    // Convert velocity to inches/second
31    let velocity_ips = velocity_mps * 39.3701;
32    
33    // Calculate revolutions per second
34    let spin_rate_rps = velocity_ips / twist_rate_inches;
35    
36    // Convert to radians per second
37    let spin_rate_rad_s = spin_rate_rps * 2.0 * PI;
38    
39    (spin_rate_rps, spin_rate_rad_s)
40}
41
42/// Calculate dynamic gyroscopic stability factor using Miller formula
43pub fn calculate_dynamic_stability(
44    bullet_mass_grains: f64,
45    velocity_mps: f64,
46    spin_rate_rad_s: f64,
47    caliber_inches: f64,
48    length_inches: f64,
49    air_density_kg_m3: f64,
50) -> f64 {
51    if spin_rate_rad_s == 0.0 || velocity_mps == 0.0 {
52        return 0.0;
53    }
54    
55    // Convert velocity to fps for Miller formula
56    let velocity_fps = velocity_mps * 3.28084;
57    
58    // Calculate twist rate in calibers
59    if caliber_inches > 0.0 {
60        // Back-calculate twist rate from spin rate
61        let spin_rps = spin_rate_rad_s / (2.0 * PI);
62        let velocity_ips = velocity_fps * 12.0; // inches per second
63        let twist_inches = if spin_rps > 0.0 { velocity_ips / spin_rps } else { 0.0 };
64        let twist_calibers = if twist_inches > 0.0 { twist_inches / caliber_inches } else { 0.0 };
65        
66        // Length to diameter ratio
67        let length_calibers = if caliber_inches > 0.0 { length_inches / caliber_inches } else { 0.0 };
68        
69        // Miller stability formula (simplified)
70        // Sg = 30 * m / (t^2 * d^3 * l * (1 + l^2))
71        // Where: m = mass in grains, t = twist in calibers, d = diameter in inches
72        //        l = length in calibers
73        
74        if twist_calibers == 0.0 || length_calibers == 0.0 {
75            return 0.0;
76        }
77        
78        let numerator = 30.0 * bullet_mass_grains;
79        let denominator = twist_calibers.powi(2) * caliber_inches.powi(3) * 
80                         length_calibers * (1.0 + length_calibers.powi(2));
81        
82        if denominator == 0.0 {
83            return 0.0;
84        }
85        
86        // Base stability
87        let sg_base = numerator / denominator;
88        
89        // Velocity correction (compared to standard 2800 fps)
90        let velocity_factor = (velocity_fps / 2800.0).powf(1.0 / 3.0);
91        
92        // Atmospheric correction
93        // Standard conditions: 59°F, 29.92 inHg = 1.225 kg/m³
94        let density_factor = (1.225 / air_density_kg_m3).sqrt();
95        
96        // Final stability
97        sg_base * velocity_factor * density_factor
98    } else {
99        0.0
100    }
101}
102
103/// Calculate the yaw of repose (equilibrium yaw angle)
104pub fn calculate_yaw_of_repose(
105    stability_factor: f64,
106    velocity_mps: f64,
107    spin_rate_rad_s: f64,
108    wind_velocity_mps: f64,
109    pitch_rate_rad_s: f64,
110    air_density_kg_m3: f64,
111    caliber_inches: f64,
112    length_inches: f64,
113    mass_grains: f64,
114    mach: f64,
115    bullet_type: &str,
116    use_pitch_damping: bool,
117) -> (f64, f64) {
118    if stability_factor <= 1.0 || spin_rate_rad_s == 0.0 {
119        return (0.0, 0.0);
120    }
121    
122    // Use enhanced calculation with pitch damping if requested
123    if use_pitch_damping && mach > 0.0 {
124        // Map bullet types for pitch damping
125        let damping_type = match bullet_type.to_lowercase().as_str() {
126            "match" => "match_boat_tail",
127            "hunting" => "hunting",
128            "fmj" => "fmj",
129            "vld" => "vld",
130            _ => "match_boat_tail",
131        };
132        
133        return calculate_damped_yaw_of_repose(
134            stability_factor, velocity_mps, spin_rate_rad_s,
135            wind_velocity_mps, pitch_rate_rad_s, air_density_kg_m3,
136            caliber_inches, length_inches, mass_grains, mach, damping_type
137        );
138    }
139    
140    // Original calculation (backward compatibility)
141    // Crosswind component creates yaw
142    let yaw_rad = if wind_velocity_mps == 0.0 {
143        // No wind - use typical value for spin drift
144        // Yaw develops due to nose following curved trajectory
145        0.002 // ~0.1 degrees typical
146    } else {
147        // Wind-induced yaw
148        if velocity_mps > 0.0 {
149            (wind_velocity_mps / velocity_mps).atan()
150        } else {
151            0.0
152        }
153    };
154    
155    // Damping factor based on stability with safe division
156    let stability_term = (stability_factor - 1.0).max(0.0).sqrt();
157    let damping = 1.0 / (1.0 + stability_term);
158    
159    (yaw_rad * damping, 0.0)  // No convergence rate in simple model
160}
161
162/// Calculate Magnus effect contribution to drift
163pub fn calculate_magnus_drift_component(
164    velocity_mps: f64,
165    spin_rate_rad_s: f64,
166    yaw_rad: f64,
167    air_density_kg_m3: f64,
168    caliber_inches: f64,
169    time_s: f64,
170    mass_grains: f64,
171) -> f64 {
172    let diameter_m = caliber_inches * 0.0254;
173    let mass_kg = mass_grains * 0.00006479891;  // Convert grains to kg
174    
175    // Magnus force coefficient (empirical)
176    // Varies with Mach number
177    let mach = velocity_mps / 343.0; // Approximate speed of sound
178    
179    let cmag = if mach < 0.8 {
180        0.25
181    } else if mach < 1.2 {
182        // Transonic reduction
183        0.15
184    } else {
185        // Supersonic
186        0.10 + 0.05 * ((mach - 1.2) / 2.0).min(1.0)
187    };
188    
189    // Spin ratio
190    let spin_ratio = (spin_rate_rad_s * diameter_m / 2.0) / velocity_mps;
191    
192    // Magnus force
193    let magnus_force = if velocity_mps > 0.0 {
194        cmag * spin_ratio * yaw_rad * 
195        0.5 * air_density_kg_m3 * velocity_mps.powi(2) * 
196        PI * (diameter_m / 2.0).powi(2)
197    } else {
198        0.0
199    };
200    
201    // Convert force to acceleration by dividing by mass
202    let magnus_accel = magnus_force / mass_kg;
203    
204    // Drift over time (simplified - should integrate)
205    
206    
207    0.5 * magnus_accel * time_s.powi(2)
208}
209
210/// Calculate pure gyroscopic drift (Poisson effect)
211pub fn calculate_gyroscopic_drift(
212    stability_factor: f64,
213    _yaw_rad: f64,
214    velocity_mps: f64,
215    time_s: f64,
216    is_right_twist: bool,
217) -> f64 {
218    if stability_factor <= 1.0 || time_s <= 0.0 {
219        return 0.0;
220    }
221    
222    // Litz formula is not reliable for subsonic flight. Disable it.
223    let velocity_fps = velocity_mps * 3.28084;
224    if velocity_fps < 1125.0 {
225        return 0.0;
226    }
227    
228    // Direction based on twist
229    let sign = if is_right_twist { 1.0 } else { -1.0 };
230    
231    // Bryan Litz's empirical formula for spin drift
232    let base_coefficient = 1.25 * (stability_factor + 1.2);
233    let time_factor = time_s.powf(1.83);
234    let drift_in = sign * base_coefficient * time_factor;
235    
236    // Convert to meters
237    
238    
239    drift_in * 0.0254
240}
241
242/// Calculate enhanced spin drift with all components
243pub fn calculate_enhanced_spin_drift(
244    bullet_mass: f64,
245    velocity_mps: f64,
246    twist_rate: f64,
247    bullet_diameter: f64,
248    bullet_length: f64,
249    is_twist_right: bool,
250    time_s: f64,
251    air_density: f64,
252    crosswind_mps: f64,
253    pitch_rate_rad_s: f64,
254    use_pitch_damping: bool,
255) -> SpinDriftComponents {
256    // Calculate initial spin rate (at muzzle)
257    let muzzle_velocity = velocity_mps; // Assuming we're passed muzzle velocity
258    let (_initial_spin_rps, initial_spin_rad_s) = calculate_spin_rate(muzzle_velocity, twist_rate);
259    
260    // Apply spin decay based on time of flight
261    let decay_params = SpinDecayParameters::from_bullet_type("match"); // Default to match for now
262    let current_spin_rad_s = update_spin_rate(
263        initial_spin_rad_s,
264        time_s,
265        velocity_mps,
266        air_density,
267        bullet_mass * 15.432358, // Convert to grains
268        bullet_diameter,
269        bullet_length,
270        Some(&decay_params),
271    );
272    
273    let spin_rps = current_spin_rad_s / (2.0 * PI);
274    let spin_rad_s = current_spin_rad_s;
275    
276    // Calculate dynamic stability
277    let stability = calculate_dynamic_stability(
278        bullet_mass,
279        velocity_mps,
280        spin_rad_s,
281        bullet_diameter,
282        bullet_length,
283        air_density,
284    );
285    
286    // Calculate Mach number for pitch damping
287    let mach = velocity_mps / 343.0; // Approximate speed of sound
288    
289    // Determine bullet type (default to match for now)
290    let bullet_type = "match";
291    
292    // Calculate yaw of repose with pitch damping
293    let (yaw_rad, convergence_rate) = calculate_yaw_of_repose(
294        stability,
295        velocity_mps,
296        spin_rad_s,
297        crosswind_mps,
298        pitch_rate_rad_s,
299        air_density,
300        bullet_diameter,
301        bullet_length,
302        bullet_mass,
303        mach,
304        bullet_type,
305        use_pitch_damping,
306    );
307    
308    // Calculate Magnus component
309    let magnus_drift = calculate_magnus_drift_component(
310        velocity_mps,
311        spin_rad_s,
312        yaw_rad,
313        air_density,
314        bullet_diameter,
315        time_s,
316        bullet_mass,
317    );
318    
319    // Calculate gyroscopic component
320    let gyro_drift = calculate_gyroscopic_drift(
321        stability,
322        yaw_rad,
323        velocity_mps,
324        time_s,
325        is_twist_right,
326    );
327    
328    // Total drift
329    let total_drift = magnus_drift + gyro_drift;
330    
331    // Drift rate (derivative)
332    let drift_rate = if time_s > 0.0 {
333        total_drift / time_s
334    } else {
335        0.0
336    };
337    
338    // Calculate pitch damping moment if using enhanced model
339    let pitch_damping_moment = if use_pitch_damping && mach > 0.0 {
340        let coeffs = PitchDampingCoefficients::from_bullet_type(bullet_type);
341        calculate_pitch_damping_moment(
342            pitch_rate_rad_s,
343            velocity_mps,
344            air_density,
345            bullet_diameter * 0.0254, // Convert to meters
346            bullet_length * 0.0254,    // Convert to meters
347            mach,
348            &coeffs,
349        )
350    } else {
351        0.0
352    };
353    
354    SpinDriftComponents {
355        spin_rate_rps: spin_rps,
356        spin_rate_rad_s: spin_rad_s,
357        stability_factor: stability,
358        yaw_of_repose_rad: yaw_rad,
359        drift_rate_mps: drift_rate,
360        total_drift_m: total_drift,
361        magnus_component_m: magnus_drift,
362        gyroscopic_component_m: gyro_drift,
363        pitch_damping_moment,
364        yaw_convergence_rate: convergence_rate,
365        pitch_rate_rad_s,
366    }
367}
368
369/// Apply enhanced spin drift acceleration to derivatives
370pub fn apply_enhanced_spin_drift(
371    derivatives: &mut [f64; 6],
372    spin_components: &SpinDriftComponents,
373    time_s: f64,
374    is_right_twist: bool,
375) {
376    if time_s > 0.1 {
377        // Calculate acceleration from drift
378        // Using second derivative of position
379        let spin_accel_z = 2.0 * spin_components.drift_rate_mps / time_s;
380        
381        // Apply based on twist direction
382        let sign = if is_right_twist { 1.0 } else { -1.0 };
383        derivatives[5] += sign * spin_accel_z;
384    }
385}
386
387/// Simplified interface for compatibility with existing code
388pub fn compute_enhanced_spin_drift_simple(
389    time_s: f64,
390    stability: f64,
391    velocity_mps: f64,
392    twist_rate: f64,
393    is_twist_right: bool,
394    _caliber: f64,
395) -> f64 {
396    if twist_rate <= 0.0 {
397        return 0.0;
398    }
399    
400    // Calculate initial spin rate
401    let (_, initial_spin_rad_s) = calculate_spin_rate(velocity_mps, twist_rate);
402    
403    // Apply simple spin decay (assume 175gr bullet)
404    let decay_params = SpinDecayParameters::from_bullet_type("match");
405    let spin_rad_s = update_spin_rate(
406        initial_spin_rad_s,
407        time_s,
408        velocity_mps,
409        1.225, // Standard air density
410        175.0, // Standard bullet weight
411        _caliber,
412        1.3, // Standard bullet length
413        Some(&decay_params),
414    );
415    
416    // Estimate yaw of repose (use simple model for compatibility)
417    let (yaw_rad, _) = calculate_yaw_of_repose(
418        stability, velocity_mps, spin_rad_s, 0.0,
419        0.0, 1.225, _caliber, 1.3, 175.0,
420        velocity_mps / 343.0, "match", false
421    );
422    
423    // Calculate gyroscopic drift (primary component)
424    
425    
426    calculate_gyroscopic_drift(
427        stability,
428        yaw_rad,
429        velocity_mps,
430        time_s,
431        is_twist_right,
432    )
433}
434
435#[cfg(test)]
436mod tests {
437    use super::*;
438    
439    #[test]
440    fn test_calculate_spin_rate() {
441        // Test with 1:10 twist at 800 m/s
442        let (rps, rad_s) = calculate_spin_rate(800.0, 10.0);
443        
444        // 800 m/s = 31496 in/s, divided by 10 = 3149.6 rps
445        assert!((rps - 3149.6).abs() < 1.0);
446        assert!((rad_s - rps * 2.0 * PI).abs() < 0.1);
447        
448        // Test with zero twist rate
449        let (rps_zero, rad_s_zero) = calculate_spin_rate(800.0, 0.0);
450        assert_eq!(rps_zero, 0.0);
451        assert_eq!(rad_s_zero, 0.0);
452    }
453    
454    #[test]
455    fn test_calculate_dynamic_stability() {
456        let sg = calculate_dynamic_stability(
457            168.0,  // grains
458            800.0,  // m/s
459            19792.0, // rad/s (from 1:10 twist)
460            0.308,  // inches
461            1.2,    // inches
462            1.225   // kg/m³
463        );
464        
465        // Should be > 1.0 for stable bullet
466        assert!(sg > 1.0);
467        assert!(sg < 10.0); // Reasonable upper bound
468    }
469    
470    #[test]
471    fn test_calculate_yaw_of_repose() {
472        let (yaw, _) = calculate_yaw_of_repose(
473            2.5,       // Sg
474            800.0,     // velocity m/s
475            19792.0,   // spin rate rad/s
476            10.0,      // crosswind m/s
477            0.0,       // pitch rate
478            1.225,     // air density
479            0.308,     // caliber
480            1.2,       // length
481            168.0,     // mass
482            2.33,      // mach
483            "match",   // bullet type
484            false      // use pitch damping
485        );
486        
487        // Should be small but non-zero
488        assert!(yaw.abs() > 0.0);
489        assert!(yaw.abs() < 0.1); // Less than ~6 degrees
490    }
491    
492    #[test]
493    fn test_enhanced_spin_drift_calculation() {
494        let components = calculate_enhanced_spin_drift(
495            168.0,  // mass grains
496            800.0,  // velocity m/s
497            10.0,   // twist rate inches
498            0.308,  // caliber inches
499            1.2,    // length inches
500            true,   // right twist
501            1.0,    // time s
502            1.225,  // air density
503            10.0,   // crosswind
504            0.0,    // pitch rate
505            false   // use pitch damping
506        );
507        
508        // Should produce non-zero drift
509        assert!(components.total_drift_m.abs() > 0.0);
510        assert!(components.spin_rate_rps > 0.0);
511        assert!(components.stability_factor > 0.0);
512    }
513    
514    #[test]
515    fn test_opposite_twist_directions() {
516        // Right twist
517        let right_drift = calculate_enhanced_spin_drift(
518            168.0, 800.0, 10.0, 0.308, 1.2, 
519            true, 1.0, 1.225, 0.0, 0.0, false
520        );
521        
522        // Left twist
523        let left_drift = calculate_enhanced_spin_drift(
524            168.0, 800.0, 10.0, 0.308, 1.2,
525            false, 1.0, 1.225, 0.0, 0.0, false
526        );
527        
528        // Should have opposite signs for gyroscopic component
529        assert!(right_drift.gyroscopic_component_m * left_drift.gyroscopic_component_m < 0.0);
530        assert!((right_drift.gyroscopic_component_m.abs() - left_drift.gyroscopic_component_m.abs()).abs() < 0.001);
531    }
532}