1use crate::BCSegmentData;
2
3#[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
17pub struct BulletTypeFactors {
19 pub drop: f64,
20 pub transition_curve: f64,
21}
22
23impl BulletType {
24 pub fn get_factors(&self) -> BulletTypeFactors {
26 match self {
27 BulletType::MatchBoatTail => BulletTypeFactors {
28 drop: 0.075, transition_curve: 0.3,
30 },
31 BulletType::MatchFlatBase => BulletTypeFactors {
32 drop: 0.10, transition_curve: 0.35,
34 },
35 BulletType::HuntingBoatTail => BulletTypeFactors {
36 drop: 0.15, transition_curve: 0.45,
38 },
39 BulletType::HuntingFlatBase => BulletTypeFactors {
40 drop: 0.20, transition_curve: 0.5,
42 },
43 BulletType::VldHighBc => BulletTypeFactors {
44 drop: 0.05, transition_curve: 0.25,
46 },
47 BulletType::Hybrid => BulletTypeFactors {
48 drop: 0.06, transition_curve: 0.28,
50 },
51 BulletType::FMJ => BulletTypeFactors {
52 drop: 0.12, transition_curve: 0.4,
54 },
55 BulletType::RoundNose => BulletTypeFactors {
56 drop: 0.35, transition_curve: 0.7,
58 },
59 BulletType::Unknown => BulletTypeFactors {
60 drop: 0.15, transition_curve: 0.5,
62 },
63 }
64 }
65}
66
67pub struct BCSegmentEstimator;
69
70impl BCSegmentEstimator {
71 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 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 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 if model_lower.contains("bt") || model_lower.contains("boat") {
92 return BulletType::MatchBoatTail;
93 }
94 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 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 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 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 if model_lower.contains("rn") || model_lower.contains("round nose") ||
128 model_lower.contains("rnsp") {
129 return BulletType::RoundNose;
130 }
131
132 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 pub fn calculate_sectional_density(weight_grains: f64, caliber_inches: f64) -> f64 {
151 if caliber_inches <= 0.0 {
154 return 0.0;
155 }
156 weight_grains / (7000.0 * caliber_inches * caliber_inches)
157 }
158
159 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 let bullet_type = Self::identify_bullet_type(model, weight, caliber, Some(base_bc));
169 let type_factors = bullet_type.get_factors();
170
171 let sd = Self::calculate_sectional_density(weight, caliber);
173
174 let sd_factor = (sd / 0.25).max(0.7).min(1.3);
177 let adjusted_drop = type_factors.drop / sd_factor;
178
179 let transition_adjustment = if drag_model == "G7" { 0.8 } else { 1.0 };
181 let _adjusted_curve = type_factors.transition_curve * transition_adjustment;
182
183 let mut segments = Vec::new();
185
186 match bullet_type {
188 BulletType::MatchBoatTail => {
189 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 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 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 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 for segment in &mut segments {
288 segment.bc_value *= sd_factor.powf(0.5);
289 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 assert!(segments.len() >= 3);
328 assert!((segments[0].bc_value - 0.450).abs() < 0.05);
330 assert!(segments[segments.len()-1].bc_value < segments[0].bc_value);
332 }
333}