ballistics_engine/
pitch_damping.rs

1//! Pitch Damping Moment Physics for Ballistics Calculations
2//!
3//! This module implements pitch damping moments that affect:
4//! - Dynamic stability during flight
5//! - Precession and nutation damping  
6//! - Yaw of repose convergence
7//! - Transonic stability transitions
8
9use std::f64::consts::PI;
10
11/// Aerodynamic damping coefficients for different flight regimes
12#[derive(Debug, Clone, Copy)]
13pub struct PitchDampingCoefficients {
14    pub subsonic: f64,       // Cmq + Cmα̇ for M < 0.8
15    pub transonic_low: f64,  // For 0.8 <= M < 1.0
16    pub transonic_high: f64, // For 1.0 <= M < 1.2 (can be destabilizing)
17    pub supersonic: f64,     // For M >= 1.2
18}
19
20impl Default for PitchDampingCoefficients {
21    fn default() -> Self {
22        Self {
23            subsonic: -0.8,
24            transonic_low: -0.3,
25            transonic_high: 0.2,
26            supersonic: -0.5,
27        }
28    }
29}
30
31impl PitchDampingCoefficients {
32    /// Get typical coefficients for different bullet types
33    pub fn from_bullet_type(bullet_type: &str) -> Self {
34        match bullet_type.to_lowercase().as_str() {
35            "match_boat_tail" => Self {
36                subsonic: -0.9,
37                transonic_low: -0.4,
38                transonic_high: 0.1,
39                supersonic: -0.6,
40            },
41            "match_flat_base" => Self {
42                subsonic: -0.7,
43                transonic_low: -0.2,
44                transonic_high: 0.3,
45                supersonic: -0.4,
46            },
47            "vld" => Self {
48                // Very Low Drag - more stable
49                subsonic: -1.0,
50                transonic_low: -0.5,
51                transonic_high: -0.1,
52                supersonic: -0.7,
53            },
54            "hunting" => Self {
55                subsonic: -0.6,
56                transonic_low: -0.1,
57                transonic_high: 0.4,
58                supersonic: -0.3,
59            },
60            "fmj" => Self {
61                subsonic: -0.7,
62                transonic_low: -0.2,
63                transonic_high: 0.2,
64                supersonic: -0.5,
65            },
66            _ => Self::default(),
67        }
68    }
69}
70
71/// Calculate pitch damping coefficient based on Mach number
72pub fn calculate_pitch_damping_coefficient(mach: f64, coeffs: &PitchDampingCoefficients) -> f64 {
73    if mach < 0.8 {
74        // Subsonic - stable damping
75        coeffs.subsonic
76    } else if mach < 1.0 {
77        // Lower transonic - decreasing stability
78        // Linear interpolation
79        let t = (mach - 0.8) / 0.2;
80        coeffs.subsonic * (1.0 - t) + coeffs.transonic_low * t
81    } else if mach < 1.2 {
82        // Upper transonic - potentially destabilizing
83        // This is where "transonic jump" occurs
84        let t = (mach - 1.0) / 0.2;
85        coeffs.transonic_low * (1.0 - t) + coeffs.transonic_high * t
86    } else {
87        // Supersonic - returns to stable
88        // Asymptotic approach to supersonic value
89        let t = ((mach - 1.2) / 0.8).min(1.0);
90        coeffs.transonic_high * (1.0 - t) + coeffs.supersonic * t
91    }
92}
93
94/// Calculate the aerodynamic moment opposing pitch motion
95pub fn calculate_pitch_damping_moment(
96    pitch_rate_rad_s: f64,
97    velocity_mps: f64,
98    air_density_kg_m3: f64,
99    caliber_m: f64,
100    _length_m: f64,
101    mach: f64,
102    coeffs: &PitchDampingCoefficients,
103) -> f64 {
104    if velocity_mps == 0.0 || pitch_rate_rad_s == 0.0 {
105        return 0.0;
106    }
107
108    // Get damping coefficient for current Mach
109    let cmq = calculate_pitch_damping_coefficient(mach, coeffs);
110
111    // Dynamic pressure
112    let q = 0.5 * air_density_kg_m3 * velocity_mps.powi(2);
113
114    // Reference area (cross-sectional)
115    let s = PI * (caliber_m / 2.0).powi(2);
116
117    // Reference length (use diameter for missiles/projectiles)
118    let d = caliber_m;
119
120    // Non-dimensional pitch rate
121    let q_nondim = pitch_rate_rad_s * d / velocity_mps;
122
123    // Pitch damping moment
124    // Negative because it opposes motion
125    q * s * d * cmq * q_nondim
126}
127
128/// Calculate moment of inertia about transverse axis (pitch/yaw)
129pub fn calculate_transverse_moment_of_inertia(
130    mass_kg: f64,
131    caliber_m: f64,
132    length_m: f64,
133    shape: &str,
134) -> f64 {
135    let radius = caliber_m / 2.0;
136
137    match shape {
138        "cylinder" => {
139            // I_transverse = m * (3*r² + L²) / 12
140            mass_kg * (3.0 * radius.powi(2) + length_m.powi(2)) / 12.0
141        }
142        "ogive" => {
143            // Ogive has more mass toward the front
144            // Approximate as 85% of cylinder value
145            let cylinder_i = mass_kg * (3.0 * radius.powi(2) + length_m.powi(2)) / 12.0;
146            0.85 * cylinder_i
147        }
148        "boat_tail" => {
149            // Boat tail has less mass at rear
150            // Approximate as 80% of cylinder value
151            let cylinder_i = mass_kg * (3.0 * radius.powi(2) + length_m.powi(2)) / 12.0;
152            0.80 * cylinder_i
153        }
154        _ => {
155            // Default to cylinder
156            mass_kg * (3.0 * radius.powi(2) + length_m.powi(2)) / 12.0
157        }
158    }
159}
160
161/// Calculate angular acceleration from moment and inertia
162pub fn calculate_angular_acceleration(moment: f64, moment_of_inertia: f64) -> f64 {
163    if moment_of_inertia > 0.0 {
164        moment / moment_of_inertia
165    } else {
166        0.0
167    }
168}
169
170/// Calculate yaw of repose with pitch damping effects
171pub fn calculate_damped_yaw_of_repose(
172    stability_factor: f64,
173    velocity_mps: f64,
174    spin_rate_rad_s: f64,
175    wind_velocity_mps: f64,
176    pitch_rate_rad_s: f64,
177    air_density_kg_m3: f64,
178    caliber_inches: f64,
179    length_inches: f64,
180    mass_grains: f64,
181    mach: f64,
182    bullet_type: &str,
183) -> (f64, f64) {
184    if stability_factor <= 1.0 || spin_rate_rad_s == 0.0 {
185        return (0.0, 0.0);
186    }
187
188    // Convert units
189    let caliber_m = caliber_inches * 0.0254;
190    let length_m = length_inches * 0.0254;
191    let mass_kg = mass_grains * 0.00006479891;
192
193    // Base yaw from crosswind or trajectory curvature
194    let base_yaw_rad = if wind_velocity_mps != 0.0 && velocity_mps > 0.0 {
195        (wind_velocity_mps / velocity_mps).atan()
196    } else {
197        // Natural yaw from curved trajectory
198        0.002 // ~0.1 degrees typical
199    };
200
201    // Get damping coefficients
202    let coeffs = PitchDampingCoefficients::from_bullet_type(bullet_type);
203
204    // Calculate pitch damping moment
205    let damping_moment = calculate_pitch_damping_moment(
206        pitch_rate_rad_s,
207        velocity_mps,
208        air_density_kg_m3,
209        caliber_m,
210        length_m,
211        mach,
212        &coeffs,
213    );
214
215    // Calculate transverse moment of inertia
216    let i_transverse =
217        calculate_transverse_moment_of_inertia(mass_kg, caliber_m, length_m, "ogive");
218
219    // Angular acceleration from damping
220    let angular_accel = calculate_angular_acceleration(damping_moment, i_transverse);
221
222    // Time constant for convergence
223    let time_constant = if angular_accel != 0.0 && pitch_rate_rad_s != 0.0 {
224        (pitch_rate_rad_s / angular_accel).abs()
225    } else {
226        10.0 // Default large value
227    };
228
229    // Convergence rate (how fast it approaches equilibrium)
230    let convergence_rate = 1.0 / time_constant;
231
232    // Modify equilibrium yaw based on stability and damping
233    let stability_factor_clamped = stability_factor.min(10.0);
234    let mut damping_factor = 1.0 / (1.0 + (stability_factor_clamped - 1.0).sqrt());
235
236    // In transonic region, damping may be positive (destabilizing)
237    if (0.8..=1.2).contains(&mach) {
238        let cmq = calculate_pitch_damping_coefficient(mach, &coeffs);
239        if cmq > 0.0 {
240            // Destabilizing - increase effective yaw
241            damping_factor *= 1.0 + cmq.abs();
242        }
243    }
244
245    let equilibrium_yaw_rad = base_yaw_rad * damping_factor;
246
247    (equilibrium_yaw_rad, convergence_rate)
248}
249
250/// Calculate precession rate including damping effects
251pub fn calculate_precession_with_damping(
252    yaw_angle_rad: f64,
253    spin_rate_rad_s: f64,
254    velocity_mps: f64,
255    pitch_damping_moment: f64,
256    transverse_inertia: f64,
257    spin_inertia: f64,
258) -> f64 {
259    if spin_rate_rad_s == 0.0 || velocity_mps == 0.0 {
260        return 0.0;
261    }
262
263    // Basic precession rate (gyroscopic)
264    let basic_precession = (spin_inertia * spin_rate_rad_s * yaw_angle_rad.sin())
265        / (transverse_inertia * velocity_mps);
266
267    // Damping modification
268    let damping_factor = if transverse_inertia > 0.0 {
269        let factor = 1.0 - (pitch_damping_moment.abs() / (transverse_inertia * velocity_mps));
270        factor.max(0.1) // Minimum 10% precession
271    } else {
272        1.0
273    };
274
275    basic_precession * damping_factor
276}
277
278#[cfg(test)]
279mod tests {
280    use super::*;
281
282    #[test]
283    fn test_pitch_damping_coefficient() {
284        let coeffs = PitchDampingCoefficients::default();
285
286        // Subsonic
287        assert_eq!(calculate_pitch_damping_coefficient(0.5, &coeffs), -0.8);
288
289        // Transonic
290        let transonic = calculate_pitch_damping_coefficient(0.9, &coeffs);
291        assert!(transonic > -0.8 && transonic < -0.3);
292
293        // Supersonic
294        let supersonic = calculate_pitch_damping_coefficient(2.0, &coeffs);
295        assert_eq!(supersonic, -0.5);
296    }
297
298    #[test]
299    fn test_pitch_damping_moment() {
300        let coeffs = PitchDampingCoefficients::default();
301        let moment = calculate_pitch_damping_moment(
302            0.1,     // pitch rate
303            300.0,   // velocity
304            1.225,   // air density
305            0.00782, // caliber (7.82mm)
306            0.033,   // length (33mm)
307            0.87,    // Mach
308            &coeffs,
309        );
310
311        // Should be negative (opposing motion)
312        assert!(moment < 0.0);
313    }
314
315    #[test]
316    fn test_bullet_type_coefficients() {
317        let types = [
318            "match_boat_tail",
319            "match_flat_base",
320            "vld",
321            "hunting",
322            "fmj",
323            "unknown",
324        ];
325
326        for bullet_type in &types {
327            let coeffs = PitchDampingCoefficients::from_bullet_type(bullet_type);
328
329            // Check that subsonic is always stabilizing (negative)
330            assert!(coeffs.subsonic < 0.0);
331
332            // Check that supersonic eventually stabilizes
333            assert!(coeffs.supersonic < 0.0);
334
335            // Check that VLD is most stable
336            if *bullet_type == "vld" {
337                let default_coeffs = PitchDampingCoefficients::default();
338                assert!(coeffs.subsonic < default_coeffs.subsonic);
339            }
340        }
341    }
342
343    #[test]
344    fn test_transonic_instability() {
345        let coeffs = PitchDampingCoefficients::from_bullet_type("hunting");
346
347        // Check that transonic high can be destabilizing (positive)
348        assert!(coeffs.transonic_high > 0.0);
349
350        // Check coefficient through transonic region
351        let mach_1_1 = calculate_pitch_damping_coefficient(1.1, &coeffs);
352
353        // Should be transitioning toward destabilizing
354        assert!(mach_1_1 > coeffs.transonic_low);
355    }
356
357    #[test]
358    fn test_transverse_moment_of_inertia() {
359        let mass_kg = 0.01134; // 175 grains
360        let caliber_m = 0.00782; // .308"
361        let length_m = 0.033; // 1.3"
362
363        let i_cylinder =
364            calculate_transverse_moment_of_inertia(mass_kg, caliber_m, length_m, "cylinder");
365        let i_ogive = calculate_transverse_moment_of_inertia(mass_kg, caliber_m, length_m, "ogive");
366        let i_boat_tail =
367            calculate_transverse_moment_of_inertia(mass_kg, caliber_m, length_m, "boat_tail");
368        let i_unknown =
369            calculate_transverse_moment_of_inertia(mass_kg, caliber_m, length_m, "unknown");
370
371        // Check relative magnitudes
372        assert!(i_cylinder > i_ogive);
373        assert!(i_ogive > i_boat_tail);
374        assert_eq!(i_cylinder, i_unknown);
375
376        // Check absolute values are reasonable
377        assert!(i_cylinder > 0.0);
378        assert!(i_cylinder < 1.0); // Should be small for a bullet
379    }
380
381    #[test]
382    fn test_angular_acceleration() {
383        let moment = -0.001; // Small damping moment
384        let inertia = 0.0001; // Small inertia
385
386        let accel = calculate_angular_acceleration(moment, inertia);
387        assert_eq!(accel, moment / inertia);
388
389        // Test zero inertia
390        let accel_zero = calculate_angular_acceleration(moment, 0.0);
391        assert_eq!(accel_zero, 0.0);
392    }
393
394    #[test]
395    fn test_damped_yaw_of_repose() {
396        let (yaw, convergence) = calculate_damped_yaw_of_repose(
397            2.5,     // stability factor
398            800.0,   // velocity m/s
399            19000.0, // spin rate rad/s
400            10.0,    // wind velocity m/s
401            0.01,    // pitch rate rad/s
402            1.225,   // air density
403            0.308,   // caliber inches
404            1.3,     // length inches
405            175.0,   // mass grains
406            0.9,     // Mach
407            "match_boat_tail",
408        );
409
410        // Should have non-zero yaw and convergence
411        assert!(yaw > 0.0);
412        assert!(yaw < 0.1); // Should be small angle
413        assert!(convergence > 0.0);
414
415        // Test with no stability (Sg <= 1)
416        let (yaw_unstable, conv_unstable) = calculate_damped_yaw_of_repose(
417            0.9,
418            800.0,
419            19000.0,
420            10.0,
421            0.01,
422            1.225,
423            0.308,
424            1.3,
425            175.0,
426            0.9,
427            "match_boat_tail",
428        );
429        assert_eq!(yaw_unstable, 0.0);
430        assert_eq!(conv_unstable, 0.0);
431    }
432
433    #[test]
434    fn test_precession_with_damping() {
435        let precession = calculate_precession_with_damping(
436            0.01,    // yaw angle rad
437            19000.0, // spin rate rad/s
438            800.0,   // velocity m/s
439            -0.001,  // pitch damping moment
440            0.0001,  // transverse inertia
441            0.00005, // spin inertia
442        );
443
444        assert!(precession > 0.0);
445
446        // Test zero spin
447        let precession_zero =
448            calculate_precession_with_damping(0.01, 0.0, 800.0, -0.001, 0.0001, 0.00005);
449        assert_eq!(precession_zero, 0.0);
450
451        // Test zero velocity
452        let precession_no_vel =
453            calculate_precession_with_damping(0.01, 19000.0, 0.0, -0.001, 0.0001, 0.00005);
454        assert_eq!(precession_no_vel, 0.0);
455    }
456
457    #[test]
458    fn test_mach_interpolation() {
459        let coeffs = PitchDampingCoefficients::default();
460
461        // Test smooth transition through Mach regimes
462        let mach_values = [0.7, 0.8, 0.9, 1.0, 1.1, 1.2, 1.5, 2.0];
463        let mut last_value = calculate_pitch_damping_coefficient(mach_values[0], &coeffs);
464
465        for &mach in &mach_values[1..] {
466            let value = calculate_pitch_damping_coefficient(mach, &coeffs);
467
468            // Check continuity (no huge jumps)
469            assert!((value - last_value).abs() < 1.0);
470            last_value = value;
471        }
472    }
473
474    #[test]
475    fn test_pitch_damping_edge_cases() {
476        let coeffs = PitchDampingCoefficients::default();
477
478        // Test zero pitch rate
479        let moment_zero_pitch =
480            calculate_pitch_damping_moment(0.0, 300.0, 1.225, 0.00782, 0.033, 0.87, &coeffs);
481        assert_eq!(moment_zero_pitch, 0.0);
482
483        // Test zero velocity
484        let moment_zero_vel =
485            calculate_pitch_damping_moment(0.1, 0.0, 1.225, 0.00782, 0.033, 0.87, &coeffs);
486        assert_eq!(moment_zero_vel, 0.0);
487    }
488
489    #[test]
490    fn test_default_implementation() {
491        let coeffs1 = PitchDampingCoefficients::default();
492        let coeffs2 = PitchDampingCoefficients::from_bullet_type("unknown");
493
494        assert_eq!(coeffs1.subsonic, coeffs2.subsonic);
495        assert_eq!(coeffs1.transonic_low, coeffs2.transonic_low);
496        assert_eq!(coeffs1.transonic_high, coeffs2.transonic_high);
497        assert_eq!(coeffs1.supersonic, coeffs2.supersonic);
498    }
499
500    #[test]
501    fn test_transonic_jump() {
502        let _coeffs = PitchDampingCoefficients::from_bullet_type("hunting");
503
504        // In transonic region, check for potential instability
505        let (yaw_subsonic, _) = calculate_damped_yaw_of_repose(
506            2.5, 250.0, 19000.0, 10.0, 0.01, 1.225, 0.308, 1.3, 175.0, 0.7, "hunting",
507        );
508
509        let (yaw_transonic, _) = calculate_damped_yaw_of_repose(
510            2.5, 343.0, 19000.0, 10.0, 0.01, 1.225, 0.308, 1.3, 175.0, 1.0, "hunting",
511        );
512
513        // Both should be valid but potentially different
514        assert!(yaw_subsonic > 0.0);
515        assert!(yaw_transonic > 0.0);
516    }
517}