ballistics_engine/
bc_estimation.rs

1use crate::BCSegmentData;
2
3/// Bullet type classification based on model name
4#[derive(Debug, Clone, Copy, PartialEq)]
5pub enum BulletType {
6    MatchBoatTail,
7    MatchFlatBase,
8    HuntingBoatTail,
9    HuntingFlatBase,
10    VldHighBc,
11    Hybrid,
12    FMJ,
13    RoundNose,
14    Unknown,
15}
16
17/// BC degradation factors for different bullet types
18pub struct BulletTypeFactors {
19    pub drop: f64,
20    pub transition_curve: f64,
21}
22
23impl BulletType {
24    /// Get degradation factors for this bullet type
25    pub fn get_factors(&self) -> BulletTypeFactors {
26        match self {
27            BulletType::MatchBoatTail => BulletTypeFactors {
28                drop: 0.075,  // 7.5% total drop for match boat tail
29                transition_curve: 0.3,
30            },
31            BulletType::MatchFlatBase => BulletTypeFactors {
32                drop: 0.10,  // 10% for match flat base
33                transition_curve: 0.35,
34            },
35            BulletType::HuntingBoatTail => BulletTypeFactors {
36                drop: 0.15,  // 15% for hunting boat tail
37                transition_curve: 0.45,
38            },
39            BulletType::HuntingFlatBase => BulletTypeFactors {
40                drop: 0.20,  // 20% for hunting flat base
41                transition_curve: 0.5,
42            },
43            BulletType::VldHighBc => BulletTypeFactors {
44                drop: 0.05,  // 5% for VLD (very low drag)
45                transition_curve: 0.25,
46            },
47            BulletType::Hybrid => BulletTypeFactors {
48                drop: 0.06,  // 6% for hybrid designs
49                transition_curve: 0.28,
50            },
51            BulletType::FMJ => BulletTypeFactors {
52                drop: 0.12,  // 12% for military ball
53                transition_curve: 0.4,
54            },
55            BulletType::RoundNose => BulletTypeFactors {
56                drop: 0.35,  // 35% for round nose
57                transition_curve: 0.7,
58            },
59            BulletType::Unknown => BulletTypeFactors {
60                drop: 0.15,  // Conservative 15%
61                transition_curve: 0.5,
62            },
63        }
64    }
65}
66
67/// BC segment estimator based on physics and known patterns
68pub struct BCSegmentEstimator;
69
70impl BCSegmentEstimator {
71    /// Identify bullet type from model name and characteristics
72    pub fn identify_bullet_type(model: &str, weight: f64, caliber: f64, bc_value: Option<f64>) -> BulletType {
73        let model_lower = model.to_lowercase();
74        
75        // VLD/High BC bullets
76        if model_lower.contains("vld") || model_lower.contains("berger") ||
77           model_lower.contains("hybrid") || model_lower.contains("elite") {
78            if model_lower.contains("hybrid") {
79                return BulletType::Hybrid;
80            }
81            return BulletType::VldHighBc;
82        }
83        
84        // Match bullets (competition/target)
85        if model_lower.contains("smk") || model_lower.contains("matchking") ||
86           model_lower.contains("match") || model_lower.contains("bthp") ||
87           model_lower.contains("competition") || model_lower.contains("target") ||
88           model_lower.contains("a-max") || model_lower.contains("eld-m") ||
89           model_lower.contains("scenar") || model_lower.contains("x-ring") {
90            // Check for boat tail
91            if model_lower.contains("bt") || model_lower.contains("boat") {
92                return BulletType::MatchBoatTail;
93            }
94            // Check if high BC indicates boat tail
95            if let Some(bc) = bc_value {
96                let sd = Self::calculate_sectional_density(weight, caliber);
97                if bc / sd > 1.6 {
98                    return BulletType::MatchBoatTail;
99                }
100            }
101            return BulletType::MatchFlatBase;
102        }
103        
104        // Hunting bullets (expanding)
105        if model_lower.contains("gameking") || model_lower.contains("hunting") ||
106           model_lower.contains("sst") || model_lower.contains("eld-x") ||
107           model_lower.contains("partition") || model_lower.contains("accubond") ||
108           model_lower.contains("core-lokt") || model_lower.contains("ballistic tip") ||
109           model_lower.contains("v-max") || model_lower.contains("hornady sp") ||
110           model_lower.contains("interlock") || model_lower.contains("tsx") {
111            // Check for boat tail
112            if model_lower.contains("bt") || model_lower.contains("boat") ||
113               model_lower.contains("sst") || model_lower.contains("accubond") {
114                return BulletType::HuntingBoatTail;
115            }
116            return BulletType::HuntingFlatBase;
117        }
118        
119        // FMJ/Military
120        if model_lower.contains("fmj") || model_lower.contains("ball") ||
121           model_lower.contains("m80") || model_lower.contains("m855") ||
122           model_lower.contains("tracer") {
123            return BulletType::FMJ;
124        }
125        
126        // Round nose
127        if model_lower.contains("rn") || model_lower.contains("round nose") ||
128           model_lower.contains("rnsp") {
129            return BulletType::RoundNose;
130        }
131        
132        // Use BC value as hint if available
133        if let Some(bc) = bc_value {
134            let sd = Self::calculate_sectional_density(weight, caliber);
135            let bc_to_sd_ratio = bc / sd;
136            
137            if bc_to_sd_ratio > 1.8 {
138                return BulletType::VldHighBc;
139            } else if bc_to_sd_ratio > 1.5 {
140                return BulletType::MatchBoatTail;
141            } else if bc_to_sd_ratio < 1.2 {
142                return BulletType::HuntingFlatBase;
143            }
144        }
145        
146        BulletType::Unknown
147    }
148    
149    /// Calculate sectional density (SD) from weight and caliber
150    pub fn calculate_sectional_density(weight_grains: f64, caliber_inches: f64) -> f64 {
151        // SD = weight / (7000 * caliber^2)
152        // Protect against division by zero or negative caliber
153        if caliber_inches <= 0.0 {
154            return 0.0;
155        }
156        weight_grains / (7000.0 * caliber_inches * caliber_inches)
157    }
158    
159    /// Estimate BC segments based on bullet characteristics
160    pub fn estimate_bc_segments(
161        base_bc: f64,
162        caliber: f64,
163        weight: f64,
164        model: &str,
165        drag_model: &str,
166    ) -> Vec<BCSegmentData> {
167        // Identify bullet type
168        let bullet_type = Self::identify_bullet_type(model, weight, caliber, Some(base_bc));
169        let type_factors = bullet_type.get_factors();
170        
171        // Calculate sectional density
172        let sd = Self::calculate_sectional_density(weight, caliber);
173        
174        // Adjust BC drop based on sectional density
175        // Higher SD = more stable BC
176        let sd_factor = (sd / 0.25).max(0.7).min(1.3);
177        let adjusted_drop = type_factors.drop / sd_factor;
178        
179        // Adjust transition curve based on drag model
180        let transition_adjustment = if drag_model == "G7" { 0.8 } else { 1.0 };
181        let _adjusted_curve = type_factors.transition_curve * transition_adjustment;
182        
183        // Generate segments based on bullet type
184        let mut segments = Vec::new();
185        
186        // Determine velocity ranges and BC retention factors
187        match bullet_type {
188            BulletType::MatchBoatTail => {
189                // Match boat tail - minimal BC degradation
190                segments.push(BCSegmentData {
191                    velocity_min: 2800.0,
192                    velocity_max: 5000.0,
193                    bc_value: base_bc * 1.000,
194                });
195                segments.push(BCSegmentData {
196                    velocity_min: 2400.0,
197                    velocity_max: 2800.0,
198                    bc_value: base_bc * 0.985,
199                });
200                segments.push(BCSegmentData {
201                    velocity_min: 2000.0,
202                    velocity_max: 2400.0,
203                    bc_value: base_bc * 0.965,
204                });
205                segments.push(BCSegmentData {
206                    velocity_min: 1600.0,
207                    velocity_max: 2000.0,
208                    bc_value: base_bc * 0.945,
209                });
210                segments.push(BCSegmentData {
211                    velocity_min: 0.0,
212                    velocity_max: 1600.0,
213                    bc_value: base_bc * 0.925,
214                });
215            },
216            BulletType::VldHighBc | BulletType::Hybrid => {
217                // VLD/Hybrid - very stable BC
218                segments.push(BCSegmentData {
219                    velocity_min: 2800.0,
220                    velocity_max: 5000.0,
221                    bc_value: base_bc * 1.000,
222                });
223                segments.push(BCSegmentData {
224                    velocity_min: 2200.0,
225                    velocity_max: 2800.0,
226                    bc_value: base_bc * 0.990,
227                });
228                segments.push(BCSegmentData {
229                    velocity_min: 1600.0,
230                    velocity_max: 2200.0,
231                    bc_value: base_bc * 0.970,
232                });
233                segments.push(BCSegmentData {
234                    velocity_min: 0.0,
235                    velocity_max: 1600.0,
236                    bc_value: base_bc * 0.950,
237                });
238            },
239            BulletType::HuntingBoatTail => {
240                // Hunting boat tail - moderate degradation
241                segments.push(BCSegmentData {
242                    velocity_min: 2600.0,
243                    velocity_max: 5000.0,
244                    bc_value: base_bc * 1.000,
245                });
246                segments.push(BCSegmentData {
247                    velocity_min: 2200.0,
248                    velocity_max: 2600.0,
249                    bc_value: base_bc * 0.960,
250                });
251                segments.push(BCSegmentData {
252                    velocity_min: 1800.0,
253                    velocity_max: 2200.0,
254                    bc_value: base_bc * 0.900,
255                });
256                segments.push(BCSegmentData {
257                    velocity_min: 0.0,
258                    velocity_max: 1800.0,
259                    bc_value: base_bc * 0.850,
260                });
261            },
262            _ => {
263                // Default degradation profile
264                segments.push(BCSegmentData {
265                    velocity_min: 2800.0,
266                    velocity_max: 5000.0,
267                    bc_value: base_bc,
268                });
269                
270                let transonic_bc = base_bc * (1.0 - adjusted_drop * 0.3);
271                segments.push(BCSegmentData {
272                    velocity_min: 1800.0,
273                    velocity_max: 2800.0,
274                    bc_value: transonic_bc,
275                });
276                
277                let subsonic_bc = base_bc * (1.0 - adjusted_drop);
278                segments.push(BCSegmentData {
279                    velocity_min: 0.0,
280                    velocity_max: 1800.0,
281                    bc_value: subsonic_bc,
282                });
283            }
284        }
285        
286        // Apply sectional density adjustment
287        for segment in &mut segments {
288            segment.bc_value *= sd_factor.powf(0.5);
289            // Ensure we don't exceed nominal BC
290            if segment.bc_value > base_bc {
291                segment.bc_value = base_bc;
292            }
293        }
294        
295        segments
296    }
297}
298
299#[cfg(test)]
300mod tests {
301    use super::*;
302    
303    #[test]
304    fn test_bullet_type_identification() {
305        assert_eq!(BCSegmentEstimator::identify_bullet_type("168gr SMK", 168.0, 0.308, None), BulletType::MatchFlatBase);
306        assert_eq!(BCSegmentEstimator::identify_bullet_type("168gr SMK BT", 168.0, 0.308, None), BulletType::MatchBoatTail);
307        assert_eq!(BCSegmentEstimator::identify_bullet_type("150gr SST", 150.0, 0.308, None), BulletType::HuntingBoatTail);
308        assert_eq!(BCSegmentEstimator::identify_bullet_type("147gr FMJ", 147.0, 0.308, None), BulletType::FMJ);
309        assert_eq!(BCSegmentEstimator::identify_bullet_type("180gr RN", 180.0, 0.308, None), BulletType::RoundNose);
310        assert_eq!(BCSegmentEstimator::identify_bullet_type("168gr VLD", 168.0, 0.308, None), BulletType::VldHighBc);
311        assert_eq!(BCSegmentEstimator::identify_bullet_type("Some bullet", 150.0, 0.308, None), BulletType::Unknown);
312    }
313    
314    #[test]
315    fn test_sectional_density() {
316        let sd = BCSegmentEstimator::calculate_sectional_density(168.0, 0.308);
317        assert!((sd - 0.253).abs() < 0.001);
318    }
319    
320    #[test]
321    fn test_bc_estimation() {
322        let segments = BCSegmentEstimator::estimate_bc_segments(
323            0.450, 0.308, 168.0, "168gr SMK", "G1"
324        );
325        
326        // Match rifles typically have 4 segments
327        assert!(segments.len() >= 3);
328        // First segment should be close to base BC
329        assert!((segments[0].bc_value - 0.450).abs() < 0.05);
330        // BC should degrade at lower velocities
331        assert!(segments[segments.len()-1].bc_value < segments[0].bc_value);
332    }
333}