1use crate::transonic_drag::{get_projectile_shape, transonic_correction, ProjectileShape};
2use crate::DragModel;
3use ndarray::ArrayD;
4use once_cell::sync::Lazy;
5use std::path::Path;
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 {
19 mach_values,
20 cd_values,
21 }
22 }
23
24 pub fn interpolate(&self, mach: f64) -> f64 {
26 let n = self.mach_values.len();
27
28 if n == 0 {
29 return 0.5; }
31
32 if n == 1 {
33 return self.cd_values[0];
34 }
35
36 if mach <= self.mach_values[0] {
38 if n >= 2 {
40 let slope = (self.cd_values[1] - self.cd_values[0])
41 / (self.mach_values[1] - self.mach_values[0]);
42 let extrapolated = self.cd_values[0] + slope * (mach - self.mach_values[0]);
43 return extrapolated.max(0.01);
45 }
46 return self.cd_values[0];
47 }
48
49 if mach >= self.mach_values[n - 1] {
50 if n >= 2 {
52 let slope = (self.cd_values[n - 1] - self.cd_values[n - 2])
53 / (self.mach_values[n - 1] - self.mach_values[n - 2]);
54 let extrapolated = self.cd_values[n - 1] + slope * (mach - self.mach_values[n - 1]);
55 return extrapolated.max(0.01);
57 }
58 return self.cd_values[n - 1];
59 }
60
61 let mut idx = 0;
63 for i in 0..n - 1 {
64 if mach >= self.mach_values[i] && mach <= self.mach_values[i + 1] {
65 idx = i;
66 break;
67 }
68 }
69
70 if idx > 0 && idx < n - 2 {
72 self.cubic_interpolate(mach, idx)
74 } else {
75 self.linear_interpolate(mach, idx)
77 }
78 }
79
80 pub fn linear_interpolate(&self, mach: f64, idx: usize) -> f64 {
82 if idx + 1 >= self.mach_values.len() || idx + 1 >= self.cd_values.len() {
84 return self.cd_values.get(idx).copied().unwrap_or(0.5);
85 }
86
87 let x0 = self.mach_values[idx];
88 let x1 = self.mach_values[idx + 1];
89 let y0 = self.cd_values[idx];
90 let y1 = self.cd_values[idx + 1];
91
92 if (x1 - x0).abs() < crate::constants::MIN_DIVISION_THRESHOLD {
93 return y0;
94 }
95
96 let t = (mach - x0) / (x1 - x0);
97 y0 + t * (y1 - y0)
98 }
99
100 pub fn cubic_interpolate(&self, mach: f64, idx: usize) -> f64 {
102 if idx == 0 || idx + 1 >= self.mach_values.len() || idx + 1 >= self.cd_values.len() {
104 return self.linear_interpolate(mach, idx);
106 }
107
108 let x = [
110 self.mach_values[idx - 1],
111 self.mach_values[idx],
112 self.mach_values[idx + 1],
113 if idx + 2 < self.mach_values.len() {
114 self.mach_values[idx + 2]
115 } else {
116 self.mach_values[idx + 1]
117 },
118 ];
119 let y = [
120 self.cd_values[idx - 1],
121 self.cd_values[idx],
122 self.cd_values[idx + 1],
123 if idx + 2 < self.cd_values.len() {
124 self.cd_values[idx + 2]
125 } else {
126 self.cd_values[idx + 1]
127 },
128 ];
129
130 let denominator = x[2] - x[1];
133 if denominator.abs() < crate::constants::MIN_DIVISION_THRESHOLD {
134 return y[1]; }
136 let t = (mach - x[1]) / denominator;
137 let t2 = t * t;
138 let t3 = t2 * t;
139
140 let a0 = -0.5 * y[0] + 1.5 * y[1] - 1.5 * y[2] + 0.5 * y[3];
141 let a1 = y[0] - 2.5 * y[1] + 2.0 * y[2] - 0.5 * y[3];
142 let a2 = -0.5 * y[0] + 0.5 * y[2];
143 let a3 = y[1];
144
145 a0 * t3 + a1 * t2 + a2 * t + a3
146 }
147}
148
149pub fn load_drag_table(
151 drag_tables_dir: &Path,
152 filename: &str,
153 fallback_data: &[(f64, f64)],
154) -> DragTable {
155 let npy_path = drag_tables_dir.join(format!("{filename}.npy"));
157 if let Ok(array) = ndarray_npy::read_npy::<_, ArrayD<f64>>(&npy_path) {
158 if let Ok(array_2d) = array.into_dimensionality::<ndarray::Ix2>() {
159 let mach_values: Vec<f64> = array_2d.column(0).to_vec();
160 let cd_values: Vec<f64> = array_2d.column(1).to_vec();
161 return DragTable::new(mach_values, cd_values);
162 }
163 }
164
165 let csv_path = drag_tables_dir.join(format!("{filename}.csv"));
167 if let Ok(mut reader) = csv::Reader::from_path(&csv_path) {
168 let mut mach_values = Vec::new();
169 let mut cd_values = Vec::new();
170
171 for record in reader.records().flatten() {
172 if record.len() >= 2 {
173 if let (Ok(mach), Ok(cd)) = (record[0].parse::<f64>(), record[1].parse::<f64>())
174 {
175 mach_values.push(mach);
176 cd_values.push(cd);
177 }
178 }
179 }
180
181 if !mach_values.is_empty() {
182 return DragTable::new(mach_values, cd_values);
183 }
184 }
185
186 let mach_values: Vec<f64> = fallback_data.iter().map(|(m, _)| *m).collect();
188 let cd_values: Vec<f64> = fallback_data.iter().map(|(_, cd)| *cd).collect();
189 DragTable::new(mach_values, cd_values)
190}
191
192fn find_drag_tables_dir() -> Option<std::path::PathBuf> {
194 let candidates = [
196 "../drag_tables",
197 "../../drag_tables",
198 "../../../drag_tables",
199 "drag_tables",
200 ];
201
202 for candidate in &candidates {
203 let path = Path::new(candidate);
204 if path.exists() && path.is_dir() {
205 return Some(path.to_path_buf());
206 }
207 }
208
209 None
210}
211
212static G1_DRAG_TABLE: Lazy<DragTable> = Lazy::new(|| {
214 let fallback_data = [
215 (0.0, 0.2629),
216 (0.5, 0.2695),
217 (0.6, 0.2752),
218 (0.7, 0.2817),
219 (0.8, 0.2902),
220 (0.9, 0.3012),
221 (1.0, 0.4805),
222 (1.1, 0.5933),
223 (1.2, 0.6318),
224 (1.3, 0.6440),
225 (1.4, 0.6444),
226 (1.5, 0.6372),
227 (1.6, 0.6252),
228 (1.7, 0.6105),
229 (1.8, 0.5956),
230 (1.9, 0.5815),
231 (2.0, 0.5934),
232 (2.5, 0.5598),
233 (3.0, 0.5133),
234 (4.0, 0.4811),
235 (5.0, 0.4988),
236 ];
237
238 if let Some(drag_dir) = find_drag_tables_dir() {
239 load_drag_table(&drag_dir, "g1", &fallback_data)
240 } else {
241 let mach_values: Vec<f64> = fallback_data.iter().map(|(m, _)| *m).collect();
243 let cd_values: Vec<f64> = fallback_data.iter().map(|(_, cd)| *cd).collect();
244 DragTable::new(mach_values, cd_values)
245 }
246});
247
248static G7_DRAG_TABLE: Lazy<DragTable> = Lazy::new(|| {
250 let fallback_data = [
251 (0.0, 0.1198),
252 (0.5, 0.1197),
253 (0.6, 0.1202),
254 (0.7, 0.1213),
255 (0.8, 0.1240),
256 (0.9, 0.1294),
257 (1.0, 0.3803),
258 (1.1, 0.4015),
259 (1.2, 0.4043),
260 (1.3, 0.3956),
261 (1.4, 0.3814),
262 (1.5, 0.3663),
263 (1.6, 0.3520),
264 (1.7, 0.3398),
265 (1.8, 0.3297),
266 (1.9, 0.3221),
267 (2.0, 0.2980),
268 (2.5, 0.2731),
269 (3.0, 0.2424),
270 (4.0, 0.2196),
271 (5.0, 0.1618),
272 ];
273
274 if let Some(drag_dir) = find_drag_tables_dir() {
275 load_drag_table(&drag_dir, "g7", &fallback_data)
276 } else {
277 let mach_values: Vec<f64> = fallback_data.iter().map(|(m, _)| *m).collect();
279 let cd_values: Vec<f64> = fallback_data.iter().map(|(_, cd)| *cd).collect();
280 DragTable::new(mach_values, cd_values)
281 }
282});
283
284static G6_DRAG_TABLE: Lazy<DragTable> = Lazy::new(|| {
287 let fallback_data = [
288 (0.0, 0.2617),
289 (0.05, 0.2553),
290 (0.10, 0.2491),
291 (0.15, 0.2432),
292 (0.20, 0.2376),
293 (0.25, 0.2324),
294 (0.30, 0.2278),
295 (0.35, 0.2238),
296 (0.40, 0.2205),
297 (0.45, 0.2177),
298 (0.50, 0.2155),
299 (0.55, 0.2138),
300 (0.60, 0.2126),
301 (0.65, 0.2121),
302 (0.70, 0.2122),
303 (0.75, 0.2132),
304 (0.80, 0.2154),
305 (0.85, 0.2194),
306 (0.875, 0.2229),
307 (0.90, 0.2297),
308 (0.925, 0.2449),
309 (0.95, 0.2732),
310 (0.975, 0.3141),
311 (1.0, 0.3597),
312 (1.025, 0.3994),
313 (1.05, 0.4261),
314 (1.075, 0.4402),
315 (1.10, 0.4465),
316 (1.125, 0.4490),
317 (1.15, 0.4497),
318 (1.175, 0.4494),
319 (1.20, 0.4482),
320 (1.225, 0.4464),
321 (1.25, 0.4441),
322 (1.30, 0.4390),
323 (1.35, 0.4336),
324 (1.40, 0.4279),
325 (1.45, 0.4221),
326 (1.50, 0.4162),
327 (1.55, 0.4102),
328 (1.60, 0.4042),
329 (1.65, 0.3981),
330 (1.70, 0.3919),
331 (1.75, 0.3855),
332 (1.80, 0.3788),
333 (1.85, 0.3721),
334 (1.90, 0.3652),
335 (1.95, 0.3583),
336 (2.0, 0.3515),
337 (2.05, 0.3447),
338 (2.10, 0.3381),
339 (2.15, 0.3314),
340 (2.20, 0.3249),
341 (2.25, 0.3185),
342 (2.30, 0.3122),
343 (2.35, 0.3060),
344 (2.40, 0.3000),
345 (2.45, 0.2941),
346 (2.50, 0.2883),
347 (2.60, 0.2772),
348 (2.70, 0.2668),
349 (2.80, 0.2574),
350 (2.90, 0.2487),
351 (3.0, 0.2407),
352 (3.10, 0.2333),
353 (3.20, 0.2265),
354 (3.30, 0.2202),
355 (3.40, 0.2144),
356 (3.50, 0.2089),
357 (3.60, 0.2039),
358 (3.70, 0.1991),
359 (3.80, 0.1947),
360 (3.90, 0.1905),
361 (4.0, 0.1866),
362 (4.20, 0.1794),
363 (4.40, 0.1730),
364 (4.60, 0.1673),
365 (4.80, 0.1621),
366 (5.0, 0.1574),
367 ];
368
369 if let Some(drag_dir) = find_drag_tables_dir() {
370 load_drag_table(&drag_dir, "g6", &fallback_data)
371 } else {
372 let mach_values: Vec<f64> = fallback_data.iter().map(|(m, _)| *m).collect();
374 let cd_values: Vec<f64> = fallback_data.iter().map(|(_, cd)| *cd).collect();
375 DragTable::new(mach_values, cd_values)
376 }
377});
378
379static G8_DRAG_TABLE: Lazy<DragTable> = Lazy::new(|| {
382 let fallback_data = [
383 (0.0, 0.2105),
384 (0.05, 0.2105),
385 (0.10, 0.2104),
386 (0.15, 0.2104),
387 (0.20, 0.2103),
388 (0.25, 0.2103),
389 (0.30, 0.2103),
390 (0.35, 0.2103),
391 (0.40, 0.2103),
392 (0.45, 0.2102),
393 (0.50, 0.2102),
394 (0.55, 0.2102),
395 (0.60, 0.2102),
396 (0.65, 0.2102),
397 (0.70, 0.2103),
398 (0.75, 0.2103),
399 (0.80, 0.2104),
400 (0.825, 0.2104),
401 (0.85, 0.2105),
402 (0.875, 0.2106),
403 (0.90, 0.2109),
404 (0.925, 0.2183),
405 (0.95, 0.2571),
406 (0.975, 0.3358),
407 (1.0, 0.4068),
408 (1.025, 0.4378),
409 (1.05, 0.4476),
410 (1.075, 0.4493),
411 (1.10, 0.4477),
412 (1.125, 0.4450),
413 (1.15, 0.4419),
414 (1.20, 0.4353),
415 (1.25, 0.4283),
416 (1.30, 0.4208),
417 (1.35, 0.4133),
418 (1.40, 0.4059),
419 (1.45, 0.3986),
420 (1.50, 0.3915),
421 (1.55, 0.3845),
422 (1.60, 0.3777),
423 (1.65, 0.3710),
424 (1.70, 0.3645),
425 (1.75, 0.3581),
426 (1.80, 0.3519),
427 (1.85, 0.3458),
428 (1.90, 0.3400),
429 (1.95, 0.3343),
430 (2.0, 0.3288),
431 (2.05, 0.3234),
432 (2.10, 0.3182),
433 (2.15, 0.3131),
434 (2.20, 0.3081),
435 (2.25, 0.3032),
436 (2.30, 0.2983),
437 (2.35, 0.2937),
438 (2.40, 0.2891),
439 (2.45, 0.2845),
440 (2.50, 0.2802),
441 (2.60, 0.2720),
442 (2.70, 0.2642),
443 (2.80, 0.2569),
444 (2.90, 0.2499),
445 (3.0, 0.2432),
446 (3.10, 0.2368),
447 (3.20, 0.2308),
448 (3.30, 0.2251),
449 (3.40, 0.2197),
450 (3.50, 0.2147),
451 (3.60, 0.2101),
452 (3.70, 0.2058),
453 (3.80, 0.2019),
454 (3.90, 0.1983),
455 (4.0, 0.1950),
456 (4.20, 0.1890),
457 (4.40, 0.1837),
458 (4.60, 0.1791),
459 (4.80, 0.1750),
460 (5.0, 0.1713),
461 ];
462
463 if let Some(drag_dir) = find_drag_tables_dir() {
464 load_drag_table(&drag_dir, "g8", &fallback_data)
465 } else {
466 let mach_values: Vec<f64> = fallback_data.iter().map(|(m, _)| *m).collect();
468 let cd_values: Vec<f64> = fallback_data.iter().map(|(_, cd)| *cd).collect();
469 DragTable::new(mach_values, cd_values)
470 }
471});
472
473pub fn get_drag_coefficient(mach: f64, drag_model: &DragModel) -> f64 {
475 match drag_model {
476 DragModel::G1 => G1_DRAG_TABLE.interpolate(mach),
477 DragModel::G6 => G6_DRAG_TABLE.interpolate(mach),
478 DragModel::G7 => G7_DRAG_TABLE.interpolate(mach),
479 DragModel::G8 => G8_DRAG_TABLE.interpolate(mach),
480 _ => G1_DRAG_TABLE.interpolate(mach), }
482}
483
484pub fn get_drag_coefficient_with_transonic(
486 mach: f64,
487 drag_model: &DragModel,
488 apply_transonic_correction: bool,
489 projectile_shape: Option<ProjectileShape>,
490 caliber: Option<f64>,
491 weight_grains: Option<f64>,
492) -> f64 {
493 let base_cd = get_drag_coefficient(mach, drag_model);
495
496 if apply_transonic_correction && (0.8..=1.3).contains(&mach) {
498 let shape = match projectile_shape {
500 Some(s) => s,
501 None => {
502 if let (Some(cal), Some(weight)) = (caliber, weight_grains) {
503 get_projectile_shape(
504 cal,
505 weight,
506 match drag_model {
507 DragModel::G1 => "G1",
508 DragModel::G6 => "G6",
509 DragModel::G7 => "G7",
510 DragModel::G8 => "G8",
511 _ => "G1", },
513 )
514 } else {
515 ProjectileShape::Spitzer }
517 }
518 };
519
520 transonic_correction(mach, base_cd, shape, true)
522 } else {
523 base_cd
524 }
525}
526
527pub fn get_drag_coefficient_full(
529 mach: f64,
530 drag_model: &DragModel,
531 apply_transonic_correction: bool,
532 apply_reynolds_correction: bool,
533 projectile_shape: Option<ProjectileShape>,
534 caliber: Option<f64>,
535 weight_grains: Option<f64>,
536 velocity_mps: Option<f64>,
537 air_density_kg_m3: Option<f64>,
538 temperature_c: Option<f64>,
539) -> f64 {
540 let mut cd = get_drag_coefficient_with_transonic(
542 mach,
543 drag_model,
544 apply_transonic_correction,
545 projectile_shape,
546 caliber,
547 weight_grains,
548 );
549
550 if apply_reynolds_correction && mach < 1.0 {
552 if let (Some(v), Some(cal), Some(rho), Some(temp)) =
553 (velocity_mps, caliber, air_density_kg_m3, temperature_c)
554 {
555 use crate::reynolds::apply_reynolds_correction;
556 cd = apply_reynolds_correction(cd, v, cal, rho, temp, mach);
557 }
558 }
559
560 cd
561}
562
563#[cfg(test)]
564mod tests {
565 use super::*;
566
567 #[test]
568 fn test_g1_drag_coefficient_interpolation() {
569 let cd = get_drag_coefficient(1.0, &DragModel::G1);
570 assert!(cd > 0.4 && cd < 0.6, "G1 CD at Mach 1.0: {cd}");
572 }
573
574 #[test]
575 fn test_g7_drag_coefficient_interpolation() {
576 let cd = get_drag_coefficient(1.0, &DragModel::G7);
577 assert!(cd > 0.3 && cd < 0.5, "G7 CD at Mach 1.0: {cd}");
579 }
580
581 #[test]
582 fn test_drag_coefficient_continuity() {
583 for mach in [0.5, 0.8, 1.0, 1.2, 1.5, 2.0, 3.0] {
585 let cd_before = get_drag_coefficient(mach - 0.01, &DragModel::G1);
586 let cd_after = get_drag_coefficient(mach + 0.01, &DragModel::G1);
587 let difference = (cd_after - cd_before).abs();
588 assert!(
589 difference < 0.05,
590 "Large discontinuity at Mach {mach}: {cd_before} vs {cd_after}"
591 );
592 }
593 }
594
595 #[test]
596 fn test_extrapolation_bounds() {
597 let cd_low = get_drag_coefficient(0.0, &DragModel::G1);
599 assert!(cd_low > 0.01 && cd_low < 0.5, "Low Mach G1: {cd_low}");
600
601 let cd_high = get_drag_coefficient(10.0, &DragModel::G1);
603 assert!(cd_high > 0.01, "High Mach G1 should be positive: {cd_high}");
604
605 let cd_low_g7 = get_drag_coefficient(0.0, &DragModel::G7);
607 assert!(
608 cd_low_g7 > 0.01,
609 "Low Mach G7 should be positive: {cd_low_g7}"
610 );
611
612 let cd_high_g7 = get_drag_coefficient(20.0, &DragModel::G7);
613 assert!(
614 cd_high_g7 >= 0.01,
615 "High Mach G7 should be positive: {cd_high_g7}"
616 );
617 }
618
619 #[test]
620 fn test_drag_table_creation() {
621 let mach_vals = vec![0.5, 1.0, 1.5, 2.0];
622 let cd_vals = vec![0.2, 0.5, 0.4, 0.3];
623 let table = DragTable::new(mach_vals, cd_vals);
624
625 assert!((table.interpolate(1.0) - 0.5).abs() < 1e-10);
627
628 let cd_interp = table.interpolate(1.25);
630 assert!(cd_interp > 0.4 && cd_interp < 0.5);
631 }
632
633 #[test]
634 fn test_drag_table_empty() {
635 let table = DragTable::new(vec![], vec![]);
636 let result = table.interpolate(1.0);
637 assert_eq!(result, 0.5); }
639
640 #[test]
641 fn test_drag_table_single_point() {
642 let table = DragTable::new(vec![1.0], vec![0.4]);
643
644 assert_eq!(table.interpolate(0.5), 0.4);
646 assert_eq!(table.interpolate(1.0), 0.4);
647 assert_eq!(table.interpolate(2.0), 0.4);
648 }
649
650 #[test]
651 fn test_drag_table_two_points() {
652 let table = DragTable::new(vec![1.0, 2.0], vec![0.4, 0.6]);
653
654 assert!((table.interpolate(1.0) - 0.4).abs() < 1e-10);
656 assert!((table.interpolate(2.0) - 0.6).abs() < 1e-10);
657
658 let mid = table.interpolate(1.5);
660 assert!((mid - 0.5).abs() < 1e-10);
661
662 let below = table.interpolate(0.5);
664 assert!(below.abs() > 1e-10); let above = table.interpolate(3.0);
667 assert!(above.abs() > 1e-10); }
669
670 #[test]
671 fn test_linear_interpolation() {
672 let table = DragTable::new(vec![0.0, 1.0, 2.0], vec![0.2, 0.5, 0.3]);
673
674 let result = table.linear_interpolate(0.5, 0);
676 assert!((result - 0.35).abs() < 1e-10);
677
678 let table_same = DragTable::new(vec![1.0, 1.0], vec![0.4, 0.6]);
680 let result_same = table_same.linear_interpolate(1.0, 0);
681 assert_eq!(result_same, 0.4); }
683
684 #[test]
685 fn test_cubic_interpolation() {
686 let table = DragTable::new(vec![0.5, 1.0, 1.5, 2.0, 2.5], vec![0.2, 0.4, 0.6, 0.5, 0.3]);
688
689 let result = table.cubic_interpolate(1.25, 1);
691
692 assert!(result > 0.3 && result < 0.7);
694
695 let linear_result = table.linear_interpolate(1.25, 1);
697 assert!((result - linear_result).abs() < 0.2);
699 }
700
701 #[test]
702 fn test_find_drag_tables_dir() {
703 let _dir = find_drag_tables_dir();
706 }
708
709 #[test]
710 fn test_load_drag_table_fallback() {
711 use std::path::Path;
712
713 let fake_dir = Path::new("/non/existent/directory");
715 let fallback_data = [(0.5, 0.2), (1.0, 0.4), (1.5, 0.3)];
716
717 let table = load_drag_table(fake_dir, "test", &fallback_data);
718
719 assert_eq!(table.mach_values.len(), 3);
721 assert_eq!(table.cd_values.len(), 3);
722 assert_eq!(table.mach_values[0], 0.5);
723 assert_eq!(table.cd_values[0], 0.2);
724 }
725
726 #[test]
727 fn test_known_drag_values() {
728 let g1_mach1 = get_drag_coefficient(1.0, &DragModel::G1);
732 assert!(
733 (g1_mach1 - 0.4805).abs() < 0.01,
734 "G1 at Mach 1.0: {g1_mach1}"
735 );
736
737 let g7_mach1 = get_drag_coefficient(1.0, &DragModel::G7);
739 assert!(
740 (g7_mach1 - 0.3803).abs() < 0.01,
741 "G7 at Mach 1.0: {g7_mach1}"
742 );
743
744 assert!(g1_mach1 > g7_mach1, "G1 should be > G7 at Mach 1.0");
746 }
747
748 #[test]
749 fn test_monotonicity_properties() {
750 let mach_values: Vec<f64> = (8..20).map(|i| i as f64 * 0.1).collect(); let g1_values: Vec<f64> = mach_values
755 .iter()
756 .map(|&m| get_drag_coefficient(m, &DragModel::G1))
757 .collect();
758
759 let max_value = g1_values.iter().copied().fold(0.0_f64, f64::max);
761 let max_index = g1_values
762 .iter()
763 .position(|&x| x == max_value)
764 .expect("Should find maximum in non-empty vector");
765 let peak_mach = mach_values
766 .get(max_index)
767 .copied()
768 .expect("Index should be valid");
769
770 assert!(
772 peak_mach > 1.0 && peak_mach < 1.6,
773 "G1 peak at Mach {peak_mach}"
774 );
775 assert!(
776 max_value > 0.5 && max_value < 1.0,
777 "G1 peak value: {max_value}"
778 );
779 }
780
781 #[test]
782 fn test_physical_constraints() {
783 let test_machs = [0.1, 0.5, 0.8, 1.0, 1.2, 1.5, 2.0, 3.0, 5.0];
784
785 for &mach in &test_machs {
786 let g1_cd = get_drag_coefficient(mach, &DragModel::G1);
787 let g7_cd = get_drag_coefficient(mach, &DragModel::G7);
788
789 assert!(g1_cd > 0.0, "G1 CD negative at Mach {mach}: {g1_cd}");
791 assert!(g7_cd > 0.0, "G7 CD negative at Mach {mach}: {g7_cd}");
792
793 assert!(g1_cd < 2.0, "G1 CD too high at Mach {mach}: {g1_cd}");
795 assert!(g7_cd < 1.5, "G7 CD too high at Mach {mach}: {g7_cd}");
796 }
797 }
798
799 #[test]
800 fn test_performance_characteristics() {
801 use std::time::Instant;
803
804 let start = Instant::now();
805
806 for i in 0..1000 {
808 let mach = 0.5 + (i as f64) * 0.004; let _g1 = get_drag_coefficient(mach, &DragModel::G1);
810 let _g7 = get_drag_coefficient(mach, &DragModel::G7);
811 }
812
813 let elapsed = start.elapsed();
814
815 assert!(
817 elapsed.as_millis() < 100,
818 "Performance test took {}ms",
819 elapsed.as_millis()
820 );
821 }
822}
823
824pub fn interpolated_bc(mach: f64, segments: &[(f64, f64)]) -> f64 {
826 if segments.is_empty() {
827 return crate::constants::BC_FALLBACK_CONSERVATIVE; }
829
830 let mach_values: Vec<f64> = segments.iter().map(|(m, _)| *m).collect();
832
833 if mach_values.is_empty() || segments.is_empty() {
835 return crate::constants::BC_FALLBACK_CONSERVATIVE; }
837
838 if let Some(first_mach) = mach_values.first() {
840 if mach <= *first_mach {
841 return segments.first().map(|(_, bc)| *bc).unwrap_or(0.5);
842 }
843 }
844
845 if let Some(last_mach) = mach_values.last() {
846 if mach >= *last_mach {
847 return segments.last().map(|(_, bc)| *bc).unwrap_or(0.5);
848 }
849 }
850
851 let idx = match mach_values
853 .binary_search_by(|&m| m.partial_cmp(&mach).unwrap_or(std::cmp::Ordering::Equal))
854 {
855 Ok(idx) => {
856 return segments.get(idx).map(|(_, bc)| *bc).unwrap_or(0.5);
858 }
859 Err(idx) => idx, };
861
862 if idx == 0 || idx >= segments.len() {
864 let safe_idx = idx.saturating_sub(1).min(segments.len().saturating_sub(1));
867 return segments.get(safe_idx).map(|(_, bc)| *bc).unwrap_or(0.5);
868 }
869
870 match (segments.get(idx - 1), segments.get(idx)) {
872 (Some((lo_mach, lo_bc)), Some((hi_mach, hi_bc))) => {
873 let denominator = hi_mach - lo_mach;
875 if denominator.abs() < crate::constants::MIN_DIVISION_THRESHOLD {
876 return *lo_bc; }
878 let frac = (mach - lo_mach) / denominator;
879 lo_bc + frac * (hi_bc - lo_bc)
880 }
881 _ => 0.5, }
883}
884
885