ballistics_engine/
transonic_drag.rs

1/// Transonic drag modeling with shock wave effects
2/// 
3/// This module implements physics-based corrections for drag in the transonic regime
4/// (Mach 0.8-1.2) where shock waves significantly affect the drag coefficient.
5
6
7#[derive(Debug, Clone, Copy, PartialEq)]
8pub enum ProjectileShape {
9    Spitzer,    // Sharp pointed (most common)
10    RoundNose,  // Blunt/round nose
11    FlatBase,   // Flat base (wadcutter)
12    BoatTail,   // Boat tail design
13}
14
15impl ProjectileShape {
16    /// Parse from string representation
17    pub fn from_str(s: &str) -> Self {
18        match s.to_lowercase().as_str() {
19            "spitzer" => Self::Spitzer,
20            "round_nose" => Self::RoundNose,
21            "flat_base" => Self::FlatBase,
22            "boat_tail" => Self::BoatTail,
23            _ => Self::Spitzer, // Default
24        }
25    }
26}
27
28/// Calculate Prandtl-Glauert correction factor for compressibility effects
29/// 
30/// This factor accounts for air compressibility effects in subsonic flow approaching Mach 1.
31/// Formula: 1/sqrt(1-M²) where M is Mach number
32///
33/// Physical basis: As flow approaches sonic speeds, local acceleration over the projectile
34/// surface causes compressibility effects that increase the effective angle of attack and drag.
35///
36/// Note: This correction is only valid for subsonic flow (Mach < 1.0)
37#[allow(dead_code)]
38fn prandtl_glauert_correction(mach: f64) -> f64 {
39    if mach >= 0.99 {
40        // Near Mach 1, the theoretical correction approaches infinity (1/sqrt(1-1²) = 1/0)
41        // Cap at 10.0 to prevent numerical divergence while maintaining physical meaning
42        // This represents a practical limit where the linear theory breaks down
43        return 10.0;
44    }
45
46    // Classic Prandtl-Glauert compressibility correction factor
47    // Accounts for increased pressure coefficients due to compressibility
48    let beta = (1.0 - mach * mach).sqrt();
49    1.0 / beta
50}
51
52/// Get the critical Mach number where transonic drag rise begins
53/// 
54/// The critical Mach number is where local flow acceleration first reaches Mach 1,
55/// causing shock wave formation even though freestream velocity is still subsonic.
56/// These values are based on wind tunnel data and computational fluid dynamics.
57fn critical_mach_number(shape: ProjectileShape) -> f64 {
58    match shape {
59        // Sharp, pointed nose delays shock formation due to gradual flow acceleration
60        // Range: 0.83-0.87 depending on nose sharpness angle
61        // Source: Aberdeen Proving Ground wind tunnel data
62        ProjectileShape::Spitzer => 0.85,
63        
64        // Blunt nose causes earlier shock formation due to rapid flow deceleration
65        // Range: 0.72-0.78 depending on nose radius
66        // Source: Classical aerodynamics literature (Hoerner, 1965)
67        ProjectileShape::RoundNose => 0.75,
68        
69        // Flat base creates additional pressure drag and early transonic effects
70        // Range: 0.68-0.72, includes base drag contributions
71        // Source: Experimental ballistics data
72        ProjectileShape::FlatBase => 0.70,
73        
74        // Tapered base (boat tail) reduces pressure drag, delays transonic rise
75        // Range: 0.86-0.90 depending on boat tail angle (typically 7-9 degrees)
76        // Source: Modern CFD validation studies
77        ProjectileShape::BoatTail => 0.88,
78    }
79}
80
81/// Calculate the transonic drag rise factor
82/// 
83/// This models the sharp increase in drag as shock waves form and strengthen
84/// in the transonic regime. Based on empirical correlations and theoretical
85/// models from Anderson's "Modern Compressible Flow" and McCoy's work.
86pub fn transonic_drag_rise(mach: f64, shape: ProjectileShape) -> f64 {
87    let m_crit = critical_mach_number(shape);
88    
89    if mach < m_crit {
90        // Below critical Mach, no significant drag rise
91        return 1.0;
92    }
93    
94    if mach < 1.0 {
95        // Subsonic drag rise (shock waves forming locally)
96        // Use a more physically accurate model based on empirical data
97        
98        // Progress through the drag rise region with safe division
99        let denominator = 1.0 - m_crit;
100        if denominator.abs() < f64::EPSILON {
101            return 1.0; // No drag rise if critical Mach is 1.0
102        }
103        let progress = (mach - m_crit) / denominator;
104        
105        if progress < 0.0 {
106            return 1.0;
107        }
108        
109        // Different rise rates for different shapes
110        let rise_factor = match shape {
111            ProjectileShape::BoatTail => {
112                // Boat tail has gentler drag rise
113                1.0 + 1.2 * progress.powi(2)
114            },
115            ProjectileShape::RoundNose => {
116                // Round nose has steeper drag rise
117                1.0 + 2.0 * progress.powf(1.5)
118            },
119            _ => {
120                // Spitzer is in between
121                1.0 + 1.5 * progress.powf(1.8)
122            }
123        };
124        
125        // Add compressibility effects near Mach 1
126        let rise_factor = if mach > 0.92 {
127            // Smoother transition
128            let comp_progress = (mach - 0.92) / 0.08;
129            let compressibility = 1.0 + 0.5 * comp_progress.powi(3);
130            rise_factor * compressibility
131        } else {
132            rise_factor
133        };
134        
135        rise_factor.min(2.5)  // More realistic cap
136    } else if mach < 1.2 {
137        // Transonic/early supersonic (bow shock forming)
138        // Peak drag occurs around Mach 1.0-1.1
139        
140        // Shape-dependent peak location
141        let peak_mach = match shape {
142            ProjectileShape::Spitzer => 1.05,
143            _ => 1.02,
144        };
145        
146        if mach <= peak_mach {
147            // Rising to peak
148            let base_rise = 1.8; // More realistic peak around 1.8x
149            let shape_factor = match shape {
150                ProjectileShape::RoundNose => 1.2,
151                _ => 1.0,
152            };
153            base_rise * shape_factor * (1.0 + 0.3 * (mach - 1.0))
154        } else {
155            // Descending from peak
156            let peak_drag = match shape {
157                ProjectileShape::RoundNose => 2.2,
158                _ => 1.8,
159            };
160            let decline_rate = 3.0; // How fast drag drops after peak
161            peak_drag * (-(decline_rate * (mach - peak_mach))).exp()
162        }
163    } else {
164        // Supersonic (Mach > 1.2)
165        // Drag decreases with Mach number as shock angle decreases
166        // This is handled by the base drag tables
167        1.0
168    }
169}
170
171/// Calculate the wave drag coefficient component
172/// 
173/// Wave drag is the additional drag caused by shock waves in transonic/supersonic flow.
174/// This is additive to the base drag coefficient.
175fn wave_drag_coefficient(mach: f64, shape: ProjectileShape) -> f64 {
176    if mach < 0.8 {
177        return 0.0;
178    }
179    
180    if mach < 1.0 {
181        // Subsonic wave drag (local shocks only)
182        let m_crit = critical_mach_number(shape);
183        if mach < m_crit {
184            return 0.0;
185        }
186        
187        // Gradual onset of wave drag with safe division
188        let denominator = 1.0 - m_crit;
189        if denominator.abs() < f64::EPSILON {
190            return 0.0; // No wave drag if critical Mach is 1.0
191        }
192        let progress = (mach - m_crit) / denominator;
193        let max_subsonic_wave = 0.1;
194        max_subsonic_wave * progress.powi(2)
195    } else {
196        // Supersonic wave drag
197        // Based on modified Whitcomb area rule
198        let fineness_ratio = 3.5; // Typical for bullets (length/diameter)
199        
200        // Wave drag coefficient for cone-cylinder
201        let cd_wave_base = 0.15 / fineness_ratio;
202        
203        // Mach number correction (wave drag decreases with Mach)
204        // Avoid division by zero at Mach 1.0
205        let mach_factor = 1.0 / (mach * mach - 1.0).max(crate::constants::MIN_MACH_THRESHOLD).sqrt();
206        
207        // Shape correction
208        let shape_factor = match shape {
209            ProjectileShape::Spitzer => 0.8,     // Good for wave drag
210            ProjectileShape::RoundNose => 1.2,   // Poor for wave drag
211            ProjectileShape::FlatBase => 1.5,    // Very poor
212            ProjectileShape::BoatTail => 0.7,    // Best for wave drag
213        };
214        
215        cd_wave_base * mach_factor * shape_factor
216    }
217}
218
219/// Apply transonic corrections to a base drag coefficient
220/// 
221/// This is the main function to use for correcting drag coefficients
222/// in the transonic regime.
223pub fn transonic_correction(
224    mach: f64,
225    base_cd: f64,
226    shape: ProjectileShape,
227    include_wave_drag: bool,
228) -> f64 {
229    // Get the drag rise factor
230    let rise_factor = transonic_drag_rise(mach, shape);
231    
232    // Apply to base drag
233    let mut corrected_cd = base_cd * rise_factor;
234    
235    // Add wave drag if requested
236    if include_wave_drag && mach > 0.8 {
237        let wave_cd = wave_drag_coefficient(mach, shape);
238        corrected_cd += wave_cd;
239    }
240    
241    corrected_cd
242}
243
244/// Estimate projectile shape from physical parameters
245/// 
246/// This is a simple heuristic based on typical bullet designs.
247pub fn get_projectile_shape(caliber: f64, weight_grains: f64, g_model: &str) -> ProjectileShape {
248    // G7 is typically used for boat tail bullets
249    if g_model == "G7" {
250        return ProjectileShape::BoatTail;
251    }
252    
253    // Heavy for caliber often means longer, boat tail design
254    let weight_per_caliber = weight_grains / caliber;
255    if weight_per_caliber > 500.0 {
256        // e.g., 175gr .308
257        return ProjectileShape::BoatTail;
258    }
259    
260    // Default to spitzer for most rifle bullets
261    if caliber < 0.35 {
262        // Rifle calibers
263        ProjectileShape::Spitzer
264    } else {
265        // Larger calibers often have round nose
266        ProjectileShape::RoundNose
267    }
268}
269
270#[cfg(test)]
271mod tests {
272    use super::*;
273
274    #[test]
275    fn test_prandtl_glauert() {
276        // Test subsonic values
277        assert!((prandtl_glauert_correction(0.5) - 1.1547).abs() < 0.001);
278        assert!((prandtl_glauert_correction(0.8) - 1.6667).abs() < 0.001);
279        assert!((prandtl_glauert_correction(0.95) - 3.2026).abs() < 0.001);
280        
281        // Test near Mach 1 capping
282        assert_eq!(prandtl_glauert_correction(0.99), 10.0);
283    }
284
285    #[test]
286    fn test_critical_mach() {
287        assert_eq!(critical_mach_number(ProjectileShape::Spitzer), 0.85);
288        assert_eq!(critical_mach_number(ProjectileShape::BoatTail), 0.88);
289        assert_eq!(critical_mach_number(ProjectileShape::FlatBase), 0.70);
290    }
291
292    #[test]
293    fn test_transonic_drag_rise() {
294        let shape = ProjectileShape::Spitzer;
295        
296        // Below critical Mach
297        assert_eq!(transonic_drag_rise(0.8, shape), 1.0);
298        
299        // In transonic rise
300        let rise_0_9 = transonic_drag_rise(0.9, shape);
301        assert!(rise_0_9 > 1.0 && rise_0_9 < 2.0);
302        
303        // Near Mach 1
304        let rise_0_98 = transonic_drag_rise(0.98, shape);
305        assert!(rise_0_98 > 2.0);
306        
307        // Past peak
308        let rise_1_1 = transonic_drag_rise(1.1, shape);
309        assert!(rise_1_1 > 1.5 && rise_1_1 < 2.5);
310    }
311
312    #[test]
313    fn test_projectile_shape_estimation() {
314        // G7 should always return boat tail
315        let shape = get_projectile_shape(0.308, 175.0, "G7");
316        assert!(matches!(shape, ProjectileShape::BoatTail));
317        
318        // Test that we get valid shapes for various inputs
319        let shape1 = get_projectile_shape(0.308, 200.0, "G1");
320        assert!(matches!(shape1, ProjectileShape::Spitzer | ProjectileShape::BoatTail | ProjectileShape::RoundNose));
321        
322        let shape2 = get_projectile_shape(0.224, 55.0, "G1");
323        assert!(matches!(shape2, ProjectileShape::Spitzer | ProjectileShape::BoatTail | ProjectileShape::RoundNose));
324        
325        let shape3 = get_projectile_shape(0.50, 300.0, "G1");
326        assert!(matches!(shape3, ProjectileShape::Spitzer | ProjectileShape::BoatTail | ProjectileShape::RoundNose));
327    }
328}
329
330// Removed Python-specific function
331
332// Removed Python-specific function