1use std::path::Path;
3use ndarray::ArrayD;
4use once_cell::sync::Lazy;
5use crate::DragModel;
6use crate::transonic_drag::{transonic_correction, get_projectile_shape, ProjectileShape};
7
8#[derive(Debug, Clone)]
10pub struct DragTable {
11 pub mach_values: Vec<f64>,
12 pub cd_values: Vec<f64>,
13}
14
15impl DragTable {
16 pub fn new(mach_values: Vec<f64>, cd_values: Vec<f64>) -> Self {
18 Self { mach_values, cd_values }
19 }
20
21 pub fn interpolate(&self, mach: f64) -> f64 {
23 let n = self.mach_values.len();
24
25 if n == 0 {
26 return 0.5; }
28
29 if n == 1 {
30 return self.cd_values[0];
31 }
32
33 if mach <= self.mach_values[0] {
35 if n >= 2 {
37 let slope = (self.cd_values[1] - self.cd_values[0]) /
38 (self.mach_values[1] - self.mach_values[0]);
39 let extrapolated = self.cd_values[0] + slope * (mach - self.mach_values[0]);
40 return extrapolated.max(0.01);
42 }
43 return self.cd_values[0];
44 }
45
46 if mach >= self.mach_values[n - 1] {
47 if n >= 2 {
49 let slope = (self.cd_values[n - 1] - self.cd_values[n - 2]) /
50 (self.mach_values[n - 1] - self.mach_values[n - 2]);
51 let extrapolated = self.cd_values[n - 1] + slope * (mach - self.mach_values[n - 1]);
52 return extrapolated.max(0.01);
54 }
55 return self.cd_values[n - 1];
56 }
57
58 let mut idx = 0;
60 for i in 0..n - 1 {
61 if mach >= self.mach_values[i] && mach <= self.mach_values[i + 1] {
62 idx = i;
63 break;
64 }
65 }
66
67 if idx > 0 && idx < n - 2 {
69 self.cubic_interpolate(mach, idx)
71 } else {
72 self.linear_interpolate(mach, idx)
74 }
75 }
76
77 pub fn linear_interpolate(&self, mach: f64, idx: usize) -> f64 {
79 if idx + 1 >= self.mach_values.len() || idx + 1 >= self.cd_values.len() {
81 return self.cd_values.get(idx).copied().unwrap_or(0.5);
82 }
83
84 let x0 = self.mach_values[idx];
85 let x1 = self.mach_values[idx + 1];
86 let y0 = self.cd_values[idx];
87 let y1 = self.cd_values[idx + 1];
88
89 if (x1 - x0).abs() < crate::constants::MIN_DIVISION_THRESHOLD {
90 return y0;
91 }
92
93 let t = (mach - x0) / (x1 - x0);
94 y0 + t * (y1 - y0)
95 }
96
97 pub fn cubic_interpolate(&self, mach: f64, idx: usize) -> f64 {
99 if idx == 0 || idx + 1 >= self.mach_values.len() ||
101 idx + 1 >= self.cd_values.len() {
102 return self.linear_interpolate(mach, idx);
104 }
105
106 let x = [
108 self.mach_values[idx - 1],
109 self.mach_values[idx],
110 self.mach_values[idx + 1],
111 if idx + 2 < self.mach_values.len() { self.mach_values[idx + 2] } else { self.mach_values[idx + 1] }
112 ];
113 let y = [
114 self.cd_values[idx - 1],
115 self.cd_values[idx],
116 self.cd_values[idx + 1],
117 if idx + 2 < self.cd_values.len() { self.cd_values[idx + 2] } else { self.cd_values[idx + 1] }
118 ];
119
120 let denominator = x[2] - x[1];
123 if denominator.abs() < crate::constants::MIN_DIVISION_THRESHOLD {
124 return y[1]; }
126 let t = (mach - x[1]) / denominator;
127 let t2 = t * t;
128 let t3 = t2 * t;
129
130 let a0 = -0.5 * y[0] + 1.5 * y[1] - 1.5 * y[2] + 0.5 * y[3];
131 let a1 = y[0] - 2.5 * y[1] + 2.0 * y[2] - 0.5 * y[3];
132 let a2 = -0.5 * y[0] + 0.5 * y[2];
133 let a3 = y[1];
134
135 a0 * t3 + a1 * t2 + a2 * t + a3
136 }
137}
138
139pub fn load_drag_table(drag_tables_dir: &Path, filename: &str, fallback_data: &[(f64, f64)]) -> DragTable {
141 let npy_path = drag_tables_dir.join(format!("{filename}.npy"));
143 if let Ok(array) = ndarray_npy::read_npy::<_, ArrayD<f64>>(&npy_path) {
144 if let Ok(array_2d) = array.into_dimensionality::<ndarray::Ix2>() {
145 let mach_values: Vec<f64> = array_2d.column(0).to_vec();
146 let cd_values: Vec<f64> = array_2d.column(1).to_vec();
147 return DragTable::new(mach_values, cd_values);
148 }
149 }
150
151 let csv_path = drag_tables_dir.join(format!("{filename}.csv"));
153 if let Ok(mut reader) = csv::Reader::from_path(&csv_path) {
154 let mut mach_values = Vec::new();
155 let mut cd_values = Vec::new();
156
157 for result in reader.records() {
158 if let Ok(record) = result {
159 if record.len() >= 2 {
160 if let (Ok(mach), Ok(cd)) = (record[0].parse::<f64>(), record[1].parse::<f64>()) {
161 mach_values.push(mach);
162 cd_values.push(cd);
163 }
164 }
165 }
166 }
167
168 if !mach_values.is_empty() {
169 return DragTable::new(mach_values, cd_values);
170 }
171 }
172
173 let mach_values: Vec<f64> = fallback_data.iter().map(|(m, _)| *m).collect();
175 let cd_values: Vec<f64> = fallback_data.iter().map(|(_, cd)| *cd).collect();
176 DragTable::new(mach_values, cd_values)
177}
178
179fn find_drag_tables_dir() -> Option<std::path::PathBuf> {
181 let candidates = [
183 "../drag_tables",
184 "../../drag_tables",
185 "../../../drag_tables",
186 "drag_tables",
187 ];
188
189 for candidate in &candidates {
190 let path = Path::new(candidate);
191 if path.exists() && path.is_dir() {
192 return Some(path.to_path_buf());
193 }
194 }
195
196 None
197}
198
199static G1_DRAG_TABLE: Lazy<DragTable> = Lazy::new(|| {
201 let fallback_data = [
202 (0.0, 0.2629),
203 (0.5, 0.2695),
204 (0.6, 0.2752),
205 (0.7, 0.2817),
206 (0.8, 0.2902),
207 (0.9, 0.3012),
208 (1.0, 0.4805),
209 (1.1, 0.5933),
210 (1.2, 0.6318),
211 (1.3, 0.6440),
212 (1.4, 0.6444),
213 (1.5, 0.6372),
214 (1.6, 0.6252),
215 (1.7, 0.6105),
216 (1.8, 0.5956),
217 (1.9, 0.5815),
218 (2.0, 0.5934),
219 (2.5, 0.5598),
220 (3.0, 0.5133),
221 (4.0, 0.4811),
222 (5.0, 0.4988),
223 ];
224
225 if let Some(drag_dir) = find_drag_tables_dir() {
226 load_drag_table(&drag_dir, "g1", &fallback_data)
227 } else {
228 let mach_values: Vec<f64> = fallback_data.iter().map(|(m, _)| *m).collect();
230 let cd_values: Vec<f64> = fallback_data.iter().map(|(_, cd)| *cd).collect();
231 DragTable::new(mach_values, cd_values)
232 }
233});
234
235static G7_DRAG_TABLE: Lazy<DragTable> = Lazy::new(|| {
237 let fallback_data = [
238 (0.0, 0.1198),
239 (0.5, 0.1197),
240 (0.6, 0.1202),
241 (0.7, 0.1213),
242 (0.8, 0.1240),
243 (0.9, 0.1294),
244 (1.0, 0.3803),
245 (1.1, 0.4015),
246 (1.2, 0.4043),
247 (1.3, 0.3956),
248 (1.4, 0.3814),
249 (1.5, 0.3663),
250 (1.6, 0.3520),
251 (1.7, 0.3398),
252 (1.8, 0.3297),
253 (1.9, 0.3221),
254 (2.0, 0.2980),
255 (2.5, 0.2731),
256 (3.0, 0.2424),
257 (4.0, 0.2196),
258 (5.0, 0.1618),
259 ];
260
261 if let Some(drag_dir) = find_drag_tables_dir() {
262 load_drag_table(&drag_dir, "g7", &fallback_data)
263 } else {
264 let mach_values: Vec<f64> = fallback_data.iter().map(|(m, _)| *m).collect();
266 let cd_values: Vec<f64> = fallback_data.iter().map(|(_, cd)| *cd).collect();
267 DragTable::new(mach_values, cd_values)
268 }
269});
270
271static G6_DRAG_TABLE: Lazy<DragTable> = Lazy::new(|| {
274 let fallback_data = [
275 (0.0, 0.2617), (0.05, 0.2553), (0.10, 0.2491), (0.15, 0.2432), (0.20, 0.2376),
276 (0.25, 0.2324), (0.30, 0.2278), (0.35, 0.2238), (0.40, 0.2205), (0.45, 0.2177),
277 (0.50, 0.2155), (0.55, 0.2138), (0.60, 0.2126), (0.65, 0.2121), (0.70, 0.2122),
278 (0.75, 0.2132), (0.80, 0.2154), (0.85, 0.2194), (0.875, 0.2229), (0.90, 0.2297),
279 (0.925, 0.2449), (0.95, 0.2732), (0.975, 0.3141), (1.0, 0.3597), (1.025, 0.3994),
280 (1.05, 0.4261), (1.075, 0.4402), (1.10, 0.4465), (1.125, 0.4490), (1.15, 0.4497),
281 (1.175, 0.4494), (1.20, 0.4482), (1.225, 0.4464), (1.25, 0.4441), (1.30, 0.4390),
282 (1.35, 0.4336), (1.40, 0.4279), (1.45, 0.4221), (1.50, 0.4162), (1.55, 0.4102),
283 (1.60, 0.4042), (1.65, 0.3981), (1.70, 0.3919), (1.75, 0.3855), (1.80, 0.3788),
284 (1.85, 0.3721), (1.90, 0.3652), (1.95, 0.3583), (2.0, 0.3515), (2.05, 0.3447),
285 (2.10, 0.3381), (2.15, 0.3314), (2.20, 0.3249), (2.25, 0.3185), (2.30, 0.3122),
286 (2.35, 0.3060), (2.40, 0.3000), (2.45, 0.2941), (2.50, 0.2883), (2.60, 0.2772),
287 (2.70, 0.2668), (2.80, 0.2574), (2.90, 0.2487), (3.0, 0.2407), (3.10, 0.2333),
288 (3.20, 0.2265), (3.30, 0.2202), (3.40, 0.2144), (3.50, 0.2089), (3.60, 0.2039),
289 (3.70, 0.1991), (3.80, 0.1947), (3.90, 0.1905), (4.0, 0.1866), (4.20, 0.1794),
290 (4.40, 0.1730), (4.60, 0.1673), (4.80, 0.1621), (5.0, 0.1574),
291 ];
292
293 if let Some(drag_dir) = find_drag_tables_dir() {
294 load_drag_table(&drag_dir, "g6", &fallback_data)
295 } else {
296 let mach_values: Vec<f64> = fallback_data.iter().map(|(m, _)| *m).collect();
298 let cd_values: Vec<f64> = fallback_data.iter().map(|(_, cd)| *cd).collect();
299 DragTable::new(mach_values, cd_values)
300 }
301});
302
303static G8_DRAG_TABLE: Lazy<DragTable> = Lazy::new(|| {
306 let fallback_data = [
307 (0.0, 0.2105), (0.05, 0.2105), (0.10, 0.2104), (0.15, 0.2104), (0.20, 0.2103),
308 (0.25, 0.2103), (0.30, 0.2103), (0.35, 0.2103), (0.40, 0.2103), (0.45, 0.2102),
309 (0.50, 0.2102), (0.55, 0.2102), (0.60, 0.2102), (0.65, 0.2102), (0.70, 0.2103),
310 (0.75, 0.2103), (0.80, 0.2104), (0.825, 0.2104), (0.85, 0.2105), (0.875, 0.2106),
311 (0.90, 0.2109), (0.925, 0.2183), (0.95, 0.2571), (0.975, 0.3358), (1.0, 0.4068),
312 (1.025, 0.4378), (1.05, 0.4476), (1.075, 0.4493), (1.10, 0.4477), (1.125, 0.4450),
313 (1.15, 0.4419), (1.20, 0.4353), (1.25, 0.4283), (1.30, 0.4208), (1.35, 0.4133),
314 (1.40, 0.4059), (1.45, 0.3986), (1.50, 0.3915), (1.55, 0.3845), (1.60, 0.3777),
315 (1.65, 0.3710), (1.70, 0.3645), (1.75, 0.3581), (1.80, 0.3519), (1.85, 0.3458),
316 (1.90, 0.3400), (1.95, 0.3343), (2.0, 0.3288), (2.05, 0.3234), (2.10, 0.3182),
317 (2.15, 0.3131), (2.20, 0.3081), (2.25, 0.3032), (2.30, 0.2983), (2.35, 0.2937),
318 (2.40, 0.2891), (2.45, 0.2845), (2.50, 0.2802), (2.60, 0.2720), (2.70, 0.2642),
319 (2.80, 0.2569), (2.90, 0.2499), (3.0, 0.2432), (3.10, 0.2368), (3.20, 0.2308),
320 (3.30, 0.2251), (3.40, 0.2197), (3.50, 0.2147), (3.60, 0.2101), (3.70, 0.2058),
321 (3.80, 0.2019), (3.90, 0.1983), (4.0, 0.1950), (4.20, 0.1890), (4.40, 0.1837),
322 (4.60, 0.1791), (4.80, 0.1750), (5.0, 0.1713),
323 ];
324
325 if let Some(drag_dir) = find_drag_tables_dir() {
326 load_drag_table(&drag_dir, "g8", &fallback_data)
327 } else {
328 let mach_values: Vec<f64> = fallback_data.iter().map(|(m, _)| *m).collect();
330 let cd_values: Vec<f64> = fallback_data.iter().map(|(_, cd)| *cd).collect();
331 DragTable::new(mach_values, cd_values)
332 }
333});
334
335pub fn get_drag_coefficient(mach: f64, drag_model: &DragModel) -> f64 {
337 match drag_model {
338 DragModel::G1 => G1_DRAG_TABLE.interpolate(mach),
339 DragModel::G6 => G6_DRAG_TABLE.interpolate(mach),
340 DragModel::G7 => G7_DRAG_TABLE.interpolate(mach),
341 DragModel::G8 => G8_DRAG_TABLE.interpolate(mach),
342 _ => G1_DRAG_TABLE.interpolate(mach), }
344}
345
346pub fn get_drag_coefficient_with_transonic(
348 mach: f64,
349 drag_model: &DragModel,
350 apply_transonic_correction: bool,
351 projectile_shape: Option<ProjectileShape>,
352 caliber: Option<f64>,
353 weight_grains: Option<f64>,
354) -> f64 {
355 let base_cd = get_drag_coefficient(mach, drag_model);
357
358 if apply_transonic_correction && (0.8..=1.3).contains(&mach) {
360 let shape = match projectile_shape {
362 Some(s) => s,
363 None => {
364 if let (Some(cal), Some(weight)) = (caliber, weight_grains) {
365 get_projectile_shape(cal, weight, match drag_model {
366 DragModel::G1 => "G1",
367 DragModel::G6 => "G6",
368 DragModel::G7 => "G7",
369 DragModel::G8 => "G8",
370 _ => "G1", })
372 } else {
373 ProjectileShape::Spitzer }
375 }
376 };
377
378 transonic_correction(mach, base_cd, shape, true)
380 } else {
381 base_cd
382 }
383}
384
385pub fn get_drag_coefficient_full(
387 mach: f64,
388 drag_model: &DragModel,
389 apply_transonic_correction: bool,
390 apply_reynolds_correction: bool,
391 projectile_shape: Option<ProjectileShape>,
392 caliber: Option<f64>,
393 weight_grains: Option<f64>,
394 velocity_mps: Option<f64>,
395 air_density_kg_m3: Option<f64>,
396 temperature_c: Option<f64>,
397) -> f64 {
398 let mut cd = get_drag_coefficient_with_transonic(
400 mach,
401 drag_model,
402 apply_transonic_correction,
403 projectile_shape,
404 caliber,
405 weight_grains,
406 );
407
408 if apply_reynolds_correction && mach < 1.0 {
410 if let (Some(v), Some(cal), Some(rho), Some(temp)) =
411 (velocity_mps, caliber, air_density_kg_m3, temperature_c) {
412 use crate::reynolds::apply_reynolds_correction;
413 cd = apply_reynolds_correction(cd, v, cal, rho, temp, mach);
414 }
415 }
416
417 cd
418}
419
420#[cfg(test)]
421mod tests {
422 use super::*;
423
424 #[test]
425 fn test_g1_drag_coefficient_interpolation() {
426 let cd = get_drag_coefficient(1.0, &DragModel::G1);
427 assert!(cd > 0.4 && cd < 0.6, "G1 CD at Mach 1.0: {cd}");
429 }
430
431 #[test]
432 fn test_g7_drag_coefficient_interpolation() {
433 let cd = get_drag_coefficient(1.0, &DragModel::G7);
434 assert!(cd > 0.3 && cd < 0.5, "G7 CD at Mach 1.0: {cd}");
436 }
437
438 #[test]
439 fn test_drag_coefficient_continuity() {
440 for mach in [0.5, 0.8, 1.0, 1.2, 1.5, 2.0, 3.0] {
442 let cd_before = get_drag_coefficient(mach - 0.01, &DragModel::G1);
443 let cd_after = get_drag_coefficient(mach + 0.01, &DragModel::G1);
444 let difference = (cd_after - cd_before).abs();
445 assert!(difference < 0.05, "Large discontinuity at Mach {mach}: {cd_before} vs {cd_after}");
446 }
447 }
448
449 #[test]
450 fn test_extrapolation_bounds() {
451 let cd_low = get_drag_coefficient(0.0, &DragModel::G1);
453 assert!(cd_low > 0.01 && cd_low < 0.5, "Low Mach G1: {cd_low}");
454
455 let cd_high = get_drag_coefficient(10.0, &DragModel::G1);
457 assert!(cd_high > 0.01, "High Mach G1 should be positive: {cd_high}");
458
459 let cd_low_g7 = get_drag_coefficient(0.0, &DragModel::G7);
461 assert!(cd_low_g7 > 0.01, "Low Mach G7 should be positive: {cd_low_g7}");
462
463 let cd_high_g7 = get_drag_coefficient(20.0, &DragModel::G7);
464 assert!(cd_high_g7 >= 0.01, "High Mach G7 should be positive: {cd_high_g7}");
465 }
466
467 #[test]
468 fn test_drag_table_creation() {
469 let mach_vals = vec![0.5, 1.0, 1.5, 2.0];
470 let cd_vals = vec![0.2, 0.5, 0.4, 0.3];
471 let table = DragTable::new(mach_vals, cd_vals);
472
473 assert!((table.interpolate(1.0) - 0.5).abs() < 1e-10);
475
476 let cd_interp = table.interpolate(1.25);
478 assert!(cd_interp > 0.4 && cd_interp < 0.5);
479 }
480
481 #[test]
482 fn test_drag_table_empty() {
483 let table = DragTable::new(vec![], vec![]);
484 let result = table.interpolate(1.0);
485 assert_eq!(result, 0.5); }
487
488 #[test]
489 fn test_drag_table_single_point() {
490 let table = DragTable::new(vec![1.0], vec![0.4]);
491
492 assert_eq!(table.interpolate(0.5), 0.4);
494 assert_eq!(table.interpolate(1.0), 0.4);
495 assert_eq!(table.interpolate(2.0), 0.4);
496 }
497
498 #[test]
499 fn test_drag_table_two_points() {
500 let table = DragTable::new(vec![1.0, 2.0], vec![0.4, 0.6]);
501
502 assert!((table.interpolate(1.0) - 0.4).abs() < 1e-10);
504 assert!((table.interpolate(2.0) - 0.6).abs() < 1e-10);
505
506 let mid = table.interpolate(1.5);
508 assert!((mid - 0.5).abs() < 1e-10);
509
510 let below = table.interpolate(0.5);
512 assert!(below.abs() > 1e-10); let above = table.interpolate(3.0);
515 assert!(above.abs() > 1e-10); }
517
518 #[test]
519 fn test_linear_interpolation() {
520 let table = DragTable::new(vec![0.0, 1.0, 2.0], vec![0.2, 0.5, 0.3]);
521
522 let result = table.linear_interpolate(0.5, 0);
524 assert!((result - 0.35).abs() < 1e-10);
525
526 let table_same = DragTable::new(vec![1.0, 1.0], vec![0.4, 0.6]);
528 let result_same = table_same.linear_interpolate(1.0, 0);
529 assert_eq!(result_same, 0.4); }
531
532 #[test]
533 fn test_cubic_interpolation() {
534 let table = DragTable::new(
536 vec![0.5, 1.0, 1.5, 2.0, 2.5],
537 vec![0.2, 0.4, 0.6, 0.5, 0.3]
538 );
539
540 let result = table.cubic_interpolate(1.25, 1);
542
543 assert!(result > 0.3 && result < 0.7);
545
546 let linear_result = table.linear_interpolate(1.25, 1);
548 assert!((result - linear_result).abs() < 0.2);
550 }
551
552 #[test]
553 fn test_find_drag_tables_dir() {
554 let _dir = find_drag_tables_dir();
557 }
559
560 #[test]
561 fn test_load_drag_table_fallback() {
562 use std::path::Path;
563
564 let fake_dir = Path::new("/non/existent/directory");
566 let fallback_data = [(0.5, 0.2), (1.0, 0.4), (1.5, 0.3)];
567
568 let table = load_drag_table(fake_dir, "test", &fallback_data);
569
570 assert_eq!(table.mach_values.len(), 3);
572 assert_eq!(table.cd_values.len(), 3);
573 assert_eq!(table.mach_values[0], 0.5);
574 assert_eq!(table.cd_values[0], 0.2);
575 }
576
577 #[test]
578 fn test_known_drag_values() {
579 let g1_mach1 = get_drag_coefficient(1.0, &DragModel::G1);
583 assert!((g1_mach1 - 0.4805).abs() < 0.01, "G1 at Mach 1.0: {g1_mach1}");
584
585 let g7_mach1 = get_drag_coefficient(1.0, &DragModel::G7);
587 assert!((g7_mach1 - 0.3803).abs() < 0.01, "G7 at Mach 1.0: {g7_mach1}");
588
589 assert!(g1_mach1 > g7_mach1, "G1 should be > G7 at Mach 1.0");
591 }
592
593 #[test]
594 fn test_monotonicity_properties() {
595 let mach_values: Vec<f64> = (8..20).map(|i| i as f64 * 0.1).collect(); let g1_values: Vec<f64> = mach_values.iter()
600 .map(|&m| get_drag_coefficient(m, &DragModel::G1))
601 .collect();
602
603 let max_value = g1_values.iter().copied().fold(0.0_f64, f64::max);
605 let max_index = g1_values.iter().position(|&x| x == max_value)
606 .expect("Should find maximum in non-empty vector");
607 let peak_mach = mach_values.get(max_index).copied()
608 .expect("Index should be valid");
609
610 assert!(peak_mach > 1.0 && peak_mach < 1.6, "G1 peak at Mach {peak_mach}");
612 assert!(max_value > 0.5 && max_value < 1.0, "G1 peak value: {max_value}");
613 }
614
615 #[test]
616 fn test_physical_constraints() {
617 let test_machs = [0.1, 0.5, 0.8, 1.0, 1.2, 1.5, 2.0, 3.0, 5.0];
618
619 for &mach in &test_machs {
620 let g1_cd = get_drag_coefficient(mach, &DragModel::G1);
621 let g7_cd = get_drag_coefficient(mach, &DragModel::G7);
622
623 assert!(g1_cd > 0.0, "G1 CD negative at Mach {mach}: {g1_cd}");
625 assert!(g7_cd > 0.0, "G7 CD negative at Mach {mach}: {g7_cd}");
626
627 assert!(g1_cd < 2.0, "G1 CD too high at Mach {mach}: {g1_cd}");
629 assert!(g7_cd < 1.5, "G7 CD too high at Mach {mach}: {g7_cd}");
630 }
631 }
632
633 #[test]
634 fn test_performance_characteristics() {
635 use std::time::Instant;
637
638 let start = Instant::now();
639
640 for i in 0..1000 {
642 let mach = 0.5 + (i as f64) * 0.004; let _g1 = get_drag_coefficient(mach, &DragModel::G1);
644 let _g7 = get_drag_coefficient(mach, &DragModel::G7);
645 }
646
647 let elapsed = start.elapsed();
648
649 assert!(elapsed.as_millis() < 100, "Performance test took {}ms", elapsed.as_millis());
651 }
652}
653
654pub fn interpolated_bc(mach: f64, segments: &[(f64, f64)]) -> f64 {
656 if segments.is_empty() {
657 return crate::constants::BC_FALLBACK_CONSERVATIVE; }
659
660 let mach_values: Vec<f64> = segments.iter().map(|(m, _)| *m).collect();
662
663 if mach_values.is_empty() || segments.is_empty() {
665 return crate::constants::BC_FALLBACK_CONSERVATIVE; }
667
668 if let Some(first_mach) = mach_values.first() {
670 if mach <= *first_mach {
671 return segments.first().map(|(_, bc)| *bc).unwrap_or(0.5);
672 }
673 }
674
675 if let Some(last_mach) = mach_values.last() {
676 if mach >= *last_mach {
677 return segments.last().map(|(_, bc)| *bc).unwrap_or(0.5);
678 }
679 }
680
681 let idx = match mach_values.binary_search_by(|&m| {
683 m.partial_cmp(&mach).unwrap_or(std::cmp::Ordering::Equal)
684 }) {
685 Ok(idx) => {
686 return segments.get(idx).map(|(_, bc)| *bc).unwrap_or(0.5);
688 }
689 Err(idx) => idx, };
691
692 if idx == 0 || idx >= segments.len() {
694 let safe_idx = idx.saturating_sub(1).min(segments.len().saturating_sub(1));
697 return segments.get(safe_idx).map(|(_, bc)| *bc).unwrap_or(0.5);
698 }
699
700 match (segments.get(idx - 1), segments.get(idx)) {
702 (Some((lo_mach, lo_bc)), Some((hi_mach, hi_bc))) => {
703 let denominator = hi_mach - lo_mach;
705 if denominator.abs() < crate::constants::MIN_DIVISION_THRESHOLD {
706 return *lo_bc; }
708 let frac = (mach - lo_mach) / denominator;
709 lo_bc + frac * (hi_bc - lo_bc)
710 }
711 _ => 0.5, }
713}
714
715