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(
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 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 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 if model_lower.contains("bt") || model_lower.contains("boat") {
106 return BulletType::MatchBoatTail;
107 }
108 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 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 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 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 if model_lower.contains("rn")
156 || model_lower.contains("round nose")
157 || model_lower.contains("rnsp")
158 {
159 return BulletType::RoundNose;
160 }
161
162 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 pub fn calculate_sectional_density(weight_grains: f64, caliber_inches: f64) -> f64 {
185 if caliber_inches <= 0.0 {
188 return 0.0;
189 }
190 weight_grains / (7000.0 * caliber_inches * caliber_inches)
191 }
192
193 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 let bullet_type = Self::identify_bullet_type(model, weight, caliber, Some(base_bc));
203 let type_factors = bullet_type.get_factors();
204
205 let sd = Self::calculate_sectional_density(weight, caliber);
207
208 let sd_factor = (sd / 0.25).max(0.7).min(1.3);
211 let adjusted_drop = type_factors.drop / sd_factor;
212
213 let transition_adjustment = if drag_model == "G7" { 0.8 } else { 1.0 };
215 let _adjusted_curve = type_factors.transition_curve * transition_adjustment;
216
217 let mut segments = Vec::new();
219
220 match bullet_type {
222 BulletType::MatchBoatTail => {
223 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 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 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 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 for segment in &mut segments {
322 segment.bc_value *= sd_factor.powf(0.5);
323 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 assert!(segments.len() >= 3);
382 assert!((segments[0].bc_value - 0.450).abs() < 0.05);
384 assert!(segments[segments.len() - 1].bc_value < segments[0].bc_value);
386 }
387}