Skip to main content

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(
73        model: &str,
74        weight: f64,
75        caliber: f64,
76        bc_value: Option<f64>,
77    ) -> BulletType {
78        let model_lower = model.to_lowercase();
79
80        // VLD/High BC bullets
81        if model_lower.contains("vld")
82            || model_lower.contains("berger")
83            || model_lower.contains("hybrid")
84            || model_lower.contains("elite")
85        {
86            if model_lower.contains("hybrid") {
87                return BulletType::Hybrid;
88            }
89            return BulletType::VldHighBc;
90        }
91
92        // Match bullets (competition/target)
93        if model_lower.contains("smk")
94            || model_lower.contains("matchking")
95            || model_lower.contains("match")
96            || model_lower.contains("bthp")
97            || model_lower.contains("competition")
98            || model_lower.contains("target")
99            || model_lower.contains("a-max")
100            || model_lower.contains("eld-m")
101            || model_lower.contains("scenar")
102            || model_lower.contains("x-ring")
103        {
104            // Check for boat tail
105            if model_lower.contains("bt") || model_lower.contains("boat") {
106                return BulletType::MatchBoatTail;
107            }
108            // Check if high BC indicates boat tail (guard sd>0: calculate_sectional_density
109            // returns 0 for non-positive caliber, which would make bc/sd == +Inf).
110            if let Some(bc) = bc_value {
111                let sd = Self::calculate_sectional_density(weight, caliber);
112                if sd > 0.0 && bc / sd > 1.6 {
113                    return BulletType::MatchBoatTail;
114                }
115            }
116            return BulletType::MatchFlatBase;
117        }
118
119        // Hunting bullets (expanding)
120        if model_lower.contains("gameking")
121            || model_lower.contains("hunting")
122            || model_lower.contains("sst")
123            || model_lower.contains("eld-x")
124            || model_lower.contains("partition")
125            || model_lower.contains("accubond")
126            || model_lower.contains("core-lokt")
127            || model_lower.contains("ballistic tip")
128            || model_lower.contains("v-max")
129            || model_lower.contains("hornady sp")
130            || model_lower.contains("interlock")
131            || model_lower.contains("tsx")
132        {
133            // Check for boat tail
134            if model_lower.contains("bt")
135                || model_lower.contains("boat")
136                || model_lower.contains("sst")
137                || model_lower.contains("accubond")
138            {
139                return BulletType::HuntingBoatTail;
140            }
141            return BulletType::HuntingFlatBase;
142        }
143
144        // FMJ/Military
145        if model_lower.contains("fmj")
146            || model_lower.contains("ball")
147            || model_lower.contains("m80")
148            || model_lower.contains("m855")
149            || model_lower.contains("tracer")
150        {
151            return BulletType::FMJ;
152        }
153
154        // Round nose
155        if model_lower.contains("rn")
156            || model_lower.contains("round nose")
157            || model_lower.contains("rnsp")
158        {
159            return BulletType::RoundNose;
160        }
161
162        // Use BC value as hint if available. Guard sd>0 (zero for non-positive caliber)
163        // so a degenerate input falls through to Unknown instead of dividing by zero
164        // (bc/0 == +Inf, which would silently classify as VldHighBc).
165        if let Some(bc) = bc_value {
166            let sd = Self::calculate_sectional_density(weight, caliber);
167            if sd > 0.0 {
168                let bc_to_sd_ratio = bc / sd;
169
170                if bc_to_sd_ratio > 1.8 {
171                    return BulletType::VldHighBc;
172                } else if bc_to_sd_ratio > 1.5 {
173                    return BulletType::MatchBoatTail;
174                } else if bc_to_sd_ratio < 1.2 {
175                    return BulletType::HuntingFlatBase;
176                }
177            }
178        }
179
180        BulletType::Unknown
181    }
182
183    /// Calculate sectional density (SD) from weight and caliber
184    pub fn calculate_sectional_density(weight_grains: f64, caliber_inches: f64) -> f64 {
185        // SD = weight / (7000 * caliber^2)
186        // Protect against division by zero or negative caliber
187        if caliber_inches <= 0.0 {
188            return 0.0;
189        }
190        weight_grains / (7000.0 * caliber_inches * caliber_inches)
191    }
192
193    /// Estimate BC segments based on bullet characteristics
194    pub fn estimate_bc_segments(
195        base_bc: f64,
196        caliber: f64,
197        weight: f64,
198        model: &str,
199        drag_model: &str,
200    ) -> Vec<BCSegmentData> {
201        // Identify bullet type
202        let bullet_type = Self::identify_bullet_type(model, weight, caliber, Some(base_bc));
203        let type_factors = bullet_type.get_factors();
204
205        // Calculate sectional density
206        let sd = Self::calculate_sectional_density(weight, caliber);
207
208        // Adjust BC drop based on sectional density
209        // Higher SD = more stable BC
210        let sd_factor = (sd / 0.25).max(0.7).min(1.3);
211        let adjusted_drop = type_factors.drop / sd_factor;
212
213        // Adjust transition curve based on drag model
214        let transition_adjustment = if drag_model == "G7" { 0.8 } else { 1.0 };
215        let _adjusted_curve = type_factors.transition_curve * transition_adjustment;
216
217        // Generate segments based on bullet type
218        let mut segments = Vec::new();
219
220        // Determine velocity ranges and BC retention factors
221        match bullet_type {
222            BulletType::MatchBoatTail => {
223                // Match boat tail - minimal BC degradation
224                segments.push(BCSegmentData {
225                    velocity_min: 2800.0,
226                    velocity_max: 5000.0,
227                    bc_value: base_bc * 1.000,
228                });
229                segments.push(BCSegmentData {
230                    velocity_min: 2400.0,
231                    velocity_max: 2800.0,
232                    bc_value: base_bc * 0.985,
233                });
234                segments.push(BCSegmentData {
235                    velocity_min: 2000.0,
236                    velocity_max: 2400.0,
237                    bc_value: base_bc * 0.965,
238                });
239                segments.push(BCSegmentData {
240                    velocity_min: 1600.0,
241                    velocity_max: 2000.0,
242                    bc_value: base_bc * 0.945,
243                });
244                segments.push(BCSegmentData {
245                    velocity_min: 0.0,
246                    velocity_max: 1600.0,
247                    bc_value: base_bc * 0.925,
248                });
249            }
250            BulletType::VldHighBc | BulletType::Hybrid => {
251                // VLD/Hybrid - very stable BC
252                segments.push(BCSegmentData {
253                    velocity_min: 2800.0,
254                    velocity_max: 5000.0,
255                    bc_value: base_bc * 1.000,
256                });
257                segments.push(BCSegmentData {
258                    velocity_min: 2200.0,
259                    velocity_max: 2800.0,
260                    bc_value: base_bc * 0.990,
261                });
262                segments.push(BCSegmentData {
263                    velocity_min: 1600.0,
264                    velocity_max: 2200.0,
265                    bc_value: base_bc * 0.970,
266                });
267                segments.push(BCSegmentData {
268                    velocity_min: 0.0,
269                    velocity_max: 1600.0,
270                    bc_value: base_bc * 0.950,
271                });
272            }
273            BulletType::HuntingBoatTail => {
274                // Hunting boat tail - moderate degradation
275                segments.push(BCSegmentData {
276                    velocity_min: 2600.0,
277                    velocity_max: 5000.0,
278                    bc_value: base_bc * 1.000,
279                });
280                segments.push(BCSegmentData {
281                    velocity_min: 2200.0,
282                    velocity_max: 2600.0,
283                    bc_value: base_bc * 0.960,
284                });
285                segments.push(BCSegmentData {
286                    velocity_min: 1800.0,
287                    velocity_max: 2200.0,
288                    bc_value: base_bc * 0.900,
289                });
290                segments.push(BCSegmentData {
291                    velocity_min: 0.0,
292                    velocity_max: 1800.0,
293                    bc_value: base_bc * 0.850,
294                });
295            }
296            _ => {
297                // Default degradation profile
298                segments.push(BCSegmentData {
299                    velocity_min: 2800.0,
300                    velocity_max: 5000.0,
301                    bc_value: base_bc,
302                });
303
304                let transonic_bc = base_bc * (1.0 - adjusted_drop * 0.3);
305                segments.push(BCSegmentData {
306                    velocity_min: 1800.0,
307                    velocity_max: 2800.0,
308                    bc_value: transonic_bc,
309                });
310
311                let subsonic_bc = base_bc * (1.0 - adjusted_drop);
312                segments.push(BCSegmentData {
313                    velocity_min: 0.0,
314                    velocity_max: 1800.0,
315                    bc_value: subsonic_bc,
316                });
317            }
318        }
319
320        // Apply sectional density adjustment
321        for segment in &mut segments {
322            segment.bc_value *= sd_factor.powf(0.5);
323            // Ensure we don't exceed nominal BC
324            if segment.bc_value > base_bc {
325                segment.bc_value = base_bc;
326            }
327        }
328
329        segments
330    }
331}
332
333#[cfg(test)]
334mod tests {
335    use super::*;
336
337    #[test]
338    fn test_bullet_type_identification() {
339        assert_eq!(
340            BCSegmentEstimator::identify_bullet_type("168gr SMK", 168.0, 0.308, None),
341            BulletType::MatchFlatBase
342        );
343        assert_eq!(
344            BCSegmentEstimator::identify_bullet_type("168gr SMK BT", 168.0, 0.308, None),
345            BulletType::MatchBoatTail
346        );
347        assert_eq!(
348            BCSegmentEstimator::identify_bullet_type("150gr SST", 150.0, 0.308, None),
349            BulletType::HuntingBoatTail
350        );
351        assert_eq!(
352            BCSegmentEstimator::identify_bullet_type("147gr FMJ", 147.0, 0.308, None),
353            BulletType::FMJ
354        );
355        assert_eq!(
356            BCSegmentEstimator::identify_bullet_type("180gr RN", 180.0, 0.308, None),
357            BulletType::RoundNose
358        );
359        assert_eq!(
360            BCSegmentEstimator::identify_bullet_type("168gr VLD", 168.0, 0.308, None),
361            BulletType::VldHighBc
362        );
363        assert_eq!(
364            BCSegmentEstimator::identify_bullet_type("Some bullet", 150.0, 0.308, None),
365            BulletType::Unknown
366        );
367    }
368
369    #[test]
370    fn test_sectional_density() {
371        let sd = BCSegmentEstimator::calculate_sectional_density(168.0, 0.308);
372        assert!((sd - 0.253).abs() < 0.001);
373    }
374
375    #[test]
376    fn test_bc_estimation() {
377        let segments =
378            BCSegmentEstimator::estimate_bc_segments(0.450, 0.308, 168.0, "168gr SMK", "G1");
379
380        // Match rifles typically have 4 segments
381        assert!(segments.len() >= 3);
382        // First segment should be close to base BC
383        assert!((segments[0].bc_value - 0.450).abs() < 0.05);
384        // BC should degrade at lower velocities
385        assert!(segments[segments.len() - 1].bc_value < segments[0].bc_value);
386    }
387}