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 {
110 let sd = Self::calculate_sectional_density(weight, caliber);
111 if bc / sd > 1.6 {
112 return BulletType::MatchBoatTail;
113 }
114 }
115 return BulletType::MatchFlatBase;
116 }
117
118 if model_lower.contains("gameking")
120 || model_lower.contains("hunting")
121 || model_lower.contains("sst")
122 || model_lower.contains("eld-x")
123 || model_lower.contains("partition")
124 || model_lower.contains("accubond")
125 || model_lower.contains("core-lokt")
126 || model_lower.contains("ballistic tip")
127 || model_lower.contains("v-max")
128 || model_lower.contains("hornady sp")
129 || model_lower.contains("interlock")
130 || model_lower.contains("tsx")
131 {
132 if model_lower.contains("bt")
134 || model_lower.contains("boat")
135 || model_lower.contains("sst")
136 || model_lower.contains("accubond")
137 {
138 return BulletType::HuntingBoatTail;
139 }
140 return BulletType::HuntingFlatBase;
141 }
142
143 if model_lower.contains("fmj")
145 || model_lower.contains("ball")
146 || model_lower.contains("m80")
147 || model_lower.contains("m855")
148 || model_lower.contains("tracer")
149 {
150 return BulletType::FMJ;
151 }
152
153 if model_lower.contains("rn")
155 || model_lower.contains("round nose")
156 || model_lower.contains("rnsp")
157 {
158 return BulletType::RoundNose;
159 }
160
161 if let Some(bc) = bc_value {
163 let sd = Self::calculate_sectional_density(weight, caliber);
164 let bc_to_sd_ratio = bc / sd;
165
166 if bc_to_sd_ratio > 1.8 {
167 return BulletType::VldHighBc;
168 } else if bc_to_sd_ratio > 1.5 {
169 return BulletType::MatchBoatTail;
170 } else if bc_to_sd_ratio < 1.2 {
171 return BulletType::HuntingFlatBase;
172 }
173 }
174
175 BulletType::Unknown
176 }
177
178 pub fn calculate_sectional_density(weight_grains: f64, caliber_inches: f64) -> f64 {
180 if caliber_inches <= 0.0 {
183 return 0.0;
184 }
185 weight_grains / (7000.0 * caliber_inches * caliber_inches)
186 }
187
188 pub fn estimate_bc_segments(
190 base_bc: f64,
191 caliber: f64,
192 weight: f64,
193 model: &str,
194 drag_model: &str,
195 ) -> Vec<BCSegmentData> {
196 let bullet_type = Self::identify_bullet_type(model, weight, caliber, Some(base_bc));
198 let type_factors = bullet_type.get_factors();
199
200 let sd = Self::calculate_sectional_density(weight, caliber);
202
203 let sd_factor = (sd / 0.25).max(0.7).min(1.3);
206 let adjusted_drop = type_factors.drop / sd_factor;
207
208 let transition_adjustment = if drag_model == "G7" { 0.8 } else { 1.0 };
210 let _adjusted_curve = type_factors.transition_curve * transition_adjustment;
211
212 let mut segments = Vec::new();
214
215 match bullet_type {
217 BulletType::MatchBoatTail => {
218 segments.push(BCSegmentData {
220 velocity_min: 2800.0,
221 velocity_max: 5000.0,
222 bc_value: base_bc * 1.000,
223 });
224 segments.push(BCSegmentData {
225 velocity_min: 2400.0,
226 velocity_max: 2800.0,
227 bc_value: base_bc * 0.985,
228 });
229 segments.push(BCSegmentData {
230 velocity_min: 2000.0,
231 velocity_max: 2400.0,
232 bc_value: base_bc * 0.965,
233 });
234 segments.push(BCSegmentData {
235 velocity_min: 1600.0,
236 velocity_max: 2000.0,
237 bc_value: base_bc * 0.945,
238 });
239 segments.push(BCSegmentData {
240 velocity_min: 0.0,
241 velocity_max: 1600.0,
242 bc_value: base_bc * 0.925,
243 });
244 }
245 BulletType::VldHighBc | BulletType::Hybrid => {
246 segments.push(BCSegmentData {
248 velocity_min: 2800.0,
249 velocity_max: 5000.0,
250 bc_value: base_bc * 1.000,
251 });
252 segments.push(BCSegmentData {
253 velocity_min: 2200.0,
254 velocity_max: 2800.0,
255 bc_value: base_bc * 0.990,
256 });
257 segments.push(BCSegmentData {
258 velocity_min: 1600.0,
259 velocity_max: 2200.0,
260 bc_value: base_bc * 0.970,
261 });
262 segments.push(BCSegmentData {
263 velocity_min: 0.0,
264 velocity_max: 1600.0,
265 bc_value: base_bc * 0.950,
266 });
267 }
268 BulletType::HuntingBoatTail => {
269 segments.push(BCSegmentData {
271 velocity_min: 2600.0,
272 velocity_max: 5000.0,
273 bc_value: base_bc * 1.000,
274 });
275 segments.push(BCSegmentData {
276 velocity_min: 2200.0,
277 velocity_max: 2600.0,
278 bc_value: base_bc * 0.960,
279 });
280 segments.push(BCSegmentData {
281 velocity_min: 1800.0,
282 velocity_max: 2200.0,
283 bc_value: base_bc * 0.900,
284 });
285 segments.push(BCSegmentData {
286 velocity_min: 0.0,
287 velocity_max: 1800.0,
288 bc_value: base_bc * 0.850,
289 });
290 }
291 _ => {
292 segments.push(BCSegmentData {
294 velocity_min: 2800.0,
295 velocity_max: 5000.0,
296 bc_value: base_bc,
297 });
298
299 let transonic_bc = base_bc * (1.0 - adjusted_drop * 0.3);
300 segments.push(BCSegmentData {
301 velocity_min: 1800.0,
302 velocity_max: 2800.0,
303 bc_value: transonic_bc,
304 });
305
306 let subsonic_bc = base_bc * (1.0 - adjusted_drop);
307 segments.push(BCSegmentData {
308 velocity_min: 0.0,
309 velocity_max: 1800.0,
310 bc_value: subsonic_bc,
311 });
312 }
313 }
314
315 for segment in &mut segments {
317 segment.bc_value *= sd_factor.powf(0.5);
318 if segment.bc_value > base_bc {
320 segment.bc_value = base_bc;
321 }
322 }
323
324 segments
325 }
326}
327
328#[cfg(test)]
329mod tests {
330 use super::*;
331
332 #[test]
333 fn test_bullet_type_identification() {
334 assert_eq!(
335 BCSegmentEstimator::identify_bullet_type("168gr SMK", 168.0, 0.308, None),
336 BulletType::MatchFlatBase
337 );
338 assert_eq!(
339 BCSegmentEstimator::identify_bullet_type("168gr SMK BT", 168.0, 0.308, None),
340 BulletType::MatchBoatTail
341 );
342 assert_eq!(
343 BCSegmentEstimator::identify_bullet_type("150gr SST", 150.0, 0.308, None),
344 BulletType::HuntingBoatTail
345 );
346 assert_eq!(
347 BCSegmentEstimator::identify_bullet_type("147gr FMJ", 147.0, 0.308, None),
348 BulletType::FMJ
349 );
350 assert_eq!(
351 BCSegmentEstimator::identify_bullet_type("180gr RN", 180.0, 0.308, None),
352 BulletType::RoundNose
353 );
354 assert_eq!(
355 BCSegmentEstimator::identify_bullet_type("168gr VLD", 168.0, 0.308, None),
356 BulletType::VldHighBc
357 );
358 assert_eq!(
359 BCSegmentEstimator::identify_bullet_type("Some bullet", 150.0, 0.308, None),
360 BulletType::Unknown
361 );
362 }
363
364 #[test]
365 fn test_sectional_density() {
366 let sd = BCSegmentEstimator::calculate_sectional_density(168.0, 0.308);
367 assert!((sd - 0.253).abs() < 0.001);
368 }
369
370 #[test]
371 fn test_bc_estimation() {
372 let segments =
373 BCSegmentEstimator::estimate_bc_segments(0.450, 0.308, 168.0, "168gr SMK", "G1");
374
375 assert!(segments.len() >= 3);
377 assert!((segments[0].bc_value - 0.450).abs() < 0.05);
379 assert!(segments[segments.len() - 1].bc_value < segments[0].bc_value);
381 }
382}