1use crate::transonic_drag::{get_projectile_shape, transonic_correction, ProjectileShape};
2use crate::DragModel;
3use ndarray::ArrayD;
4use std::sync::LazyLock;
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 idx = self
65 .mach_values
66 .partition_point(|&m| m < mach)
67 .saturating_sub(1)
68 .min(n - 2);
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: LazyLock<DragTable> = LazyLock::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: LazyLock<DragTable> = LazyLock::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: LazyLock<DragTable> = LazyLock::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: LazyLock<DragTable> = LazyLock::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 {
482 match drag_model {
483 DragModel::G1 => G1_DRAG_TABLE.interpolate(mach),
484 DragModel::G6 => G6_DRAG_TABLE.interpolate(mach),
485 DragModel::G7 => G7_DRAG_TABLE.interpolate(mach),
486 DragModel::G8 => G8_DRAG_TABLE.interpolate(mach),
487 DragModel::G2 | DragModel::G5 | DragModel::GI | DragModel::GS => {
489 G1_DRAG_TABLE.interpolate(mach)
490 }
491 }
492}
493
494pub fn get_drag_coefficient_with_transonic(
496 mach: f64,
497 drag_model: &DragModel,
498 apply_transonic_correction: bool,
499 projectile_shape: Option<ProjectileShape>,
500 caliber: Option<f64>,
501 weight_grains: Option<f64>,
502) -> f64 {
503 let base_cd = get_drag_coefficient(mach, drag_model);
505
506 if apply_transonic_correction && (0.8..=1.3).contains(&mach) {
508 let shape = match projectile_shape {
510 Some(s) => s,
511 None => {
512 if let (Some(cal), Some(weight)) = (caliber, weight_grains) {
513 get_projectile_shape(
514 cal,
515 weight,
516 match drag_model {
517 DragModel::G1 => "G1",
518 DragModel::G6 => "G6",
519 DragModel::G7 => "G7",
520 DragModel::G8 => "G8",
521 _ => "G1", },
523 )
524 } else {
525 ProjectileShape::Spitzer }
527 }
528 };
529
530 transonic_correction(mach, base_cd, shape, true)
532 } else {
533 base_cd
534 }
535}
536
537pub fn get_drag_coefficient_full(
539 mach: f64,
540 drag_model: &DragModel,
541 apply_transonic_correction: bool,
542 apply_reynolds_correction: bool,
543 projectile_shape: Option<ProjectileShape>,
544 caliber: Option<f64>,
545 weight_grains: Option<f64>,
546 velocity_mps: Option<f64>,
547 air_density_kg_m3: Option<f64>,
548 temperature_c: Option<f64>,
549) -> f64 {
550 let mut cd = get_drag_coefficient_with_transonic(
552 mach,
553 drag_model,
554 apply_transonic_correction,
555 projectile_shape,
556 caliber,
557 weight_grains,
558 );
559
560 if apply_reynolds_correction && mach < 1.0 {
562 if let (Some(v), Some(cal), Some(rho), Some(temp)) =
563 (velocity_mps, caliber, air_density_kg_m3, temperature_c)
564 {
565 use crate::reynolds::apply_reynolds_correction;
566 cd = apply_reynolds_correction(cd, v, cal, rho, temp, mach);
567 }
568 }
569
570 cd
571}
572
573#[cfg(test)]
574mod tests {
575 use super::*;
576
577 #[test]
578 fn test_g1_drag_coefficient_interpolation() {
579 let cd = get_drag_coefficient(1.0, &DragModel::G1);
580 assert!(cd > 0.4 && cd < 0.6, "G1 CD at Mach 1.0: {cd}");
582 }
583
584 #[test]
585 fn test_g7_drag_coefficient_interpolation() {
586 let cd = get_drag_coefficient(1.0, &DragModel::G7);
587 assert!(cd > 0.3 && cd < 0.5, "G7 CD at Mach 1.0: {cd}");
589 }
590
591 #[test]
592 fn test_drag_coefficient_continuity() {
593 for mach in [0.5, 0.8, 1.0, 1.2, 1.5, 2.0, 3.0] {
595 let cd_before = get_drag_coefficient(mach - 0.01, &DragModel::G1);
596 let cd_after = get_drag_coefficient(mach + 0.01, &DragModel::G1);
597 let difference = (cd_after - cd_before).abs();
598 assert!(
599 difference < 0.05,
600 "Large discontinuity at Mach {mach}: {cd_before} vs {cd_after}"
601 );
602 }
603 }
604
605 #[test]
606 fn test_extrapolation_bounds() {
607 let cd_low = get_drag_coefficient(0.0, &DragModel::G1);
609 assert!(cd_low > 0.01 && cd_low < 0.5, "Low Mach G1: {cd_low}");
610
611 let cd_high = get_drag_coefficient(10.0, &DragModel::G1);
613 assert!(cd_high > 0.01, "High Mach G1 should be positive: {cd_high}");
614
615 let cd_low_g7 = get_drag_coefficient(0.0, &DragModel::G7);
617 assert!(
618 cd_low_g7 > 0.01,
619 "Low Mach G7 should be positive: {cd_low_g7}"
620 );
621
622 let cd_high_g7 = get_drag_coefficient(20.0, &DragModel::G7);
623 assert!(
624 cd_high_g7 >= 0.01,
625 "High Mach G7 should be positive: {cd_high_g7}"
626 );
627 }
628
629 #[test]
630 fn test_drag_table_creation() {
631 let mach_vals = vec![0.5, 1.0, 1.5, 2.0];
632 let cd_vals = vec![0.2, 0.5, 0.4, 0.3];
633 let table = DragTable::new(mach_vals, cd_vals);
634
635 assert!((table.interpolate(1.0) - 0.5).abs() < 1e-10);
637
638 let cd_interp = table.interpolate(1.25);
640 assert!(cd_interp > 0.4 && cd_interp < 0.5);
641 }
642
643 #[test]
644 fn test_drag_table_empty() {
645 let table = DragTable::new(vec![], vec![]);
646 let result = table.interpolate(1.0);
647 assert_eq!(result, 0.5); }
649
650 #[test]
651 fn test_drag_table_single_point() {
652 let table = DragTable::new(vec![1.0], vec![0.4]);
653
654 assert_eq!(table.interpolate(0.5), 0.4);
656 assert_eq!(table.interpolate(1.0), 0.4);
657 assert_eq!(table.interpolate(2.0), 0.4);
658 }
659
660 #[test]
661 fn test_drag_table_two_points() {
662 let table = DragTable::new(vec![1.0, 2.0], vec![0.4, 0.6]);
663
664 assert!((table.interpolate(1.0) - 0.4).abs() < 1e-10);
666 assert!((table.interpolate(2.0) - 0.6).abs() < 1e-10);
667
668 let mid = table.interpolate(1.5);
670 assert!((mid - 0.5).abs() < 1e-10);
671
672 let below = table.interpolate(0.5);
674 assert!(below.abs() > 1e-10); let above = table.interpolate(3.0);
677 assert!(above.abs() > 1e-10); }
679
680 #[test]
681 fn test_linear_interpolation() {
682 let table = DragTable::new(vec![0.0, 1.0, 2.0], vec![0.2, 0.5, 0.3]);
683
684 let result = table.linear_interpolate(0.5, 0);
686 assert!((result - 0.35).abs() < 1e-10);
687
688 let table_same = DragTable::new(vec![1.0, 1.0], vec![0.4, 0.6]);
690 let result_same = table_same.linear_interpolate(1.0, 0);
691 assert_eq!(result_same, 0.4); }
693
694 #[test]
695 fn test_cubic_interpolation() {
696 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]);
698
699 let result = table.cubic_interpolate(1.25, 1);
701
702 assert!(result > 0.3 && result < 0.7);
704
705 let linear_result = table.linear_interpolate(1.25, 1);
707 assert!((result - linear_result).abs() < 0.2);
709 }
710
711 #[test]
712 fn test_find_drag_tables_dir() {
713 let _dir = find_drag_tables_dir();
716 }
718
719 #[test]
720 fn test_load_drag_table_fallback() {
721 use std::path::Path;
722
723 let fake_dir = Path::new("/non/existent/directory");
725 let fallback_data = [(0.5, 0.2), (1.0, 0.4), (1.5, 0.3)];
726
727 let table = load_drag_table(fake_dir, "test", &fallback_data);
728
729 assert_eq!(table.mach_values.len(), 3);
731 assert_eq!(table.cd_values.len(), 3);
732 assert_eq!(table.mach_values[0], 0.5);
733 assert_eq!(table.cd_values[0], 0.2);
734 }
735
736 #[test]
737 fn test_known_drag_values() {
738 let g1_mach1 = get_drag_coefficient(1.0, &DragModel::G1);
742 assert!(
743 (g1_mach1 - 0.4805).abs() < 0.01,
744 "G1 at Mach 1.0: {g1_mach1}"
745 );
746
747 let g7_mach1 = get_drag_coefficient(1.0, &DragModel::G7);
749 assert!(
750 (g7_mach1 - 0.3803).abs() < 0.01,
751 "G7 at Mach 1.0: {g7_mach1}"
752 );
753
754 assert!(g1_mach1 > g7_mach1, "G1 should be > G7 at Mach 1.0");
756 }
757
758 #[test]
759 fn test_monotonicity_properties() {
760 let mach_values: Vec<f64> = (8..20).map(|i| i as f64 * 0.1).collect(); let g1_values: Vec<f64> = mach_values
765 .iter()
766 .map(|&m| get_drag_coefficient(m, &DragModel::G1))
767 .collect();
768
769 let max_value = g1_values.iter().copied().fold(0.0_f64, f64::max);
771 let max_index = g1_values
772 .iter()
773 .position(|&x| x == max_value)
774 .expect("Should find maximum in non-empty vector");
775 let peak_mach = mach_values
776 .get(max_index)
777 .copied()
778 .expect("Index should be valid");
779
780 assert!(
782 peak_mach > 1.0 && peak_mach < 1.6,
783 "G1 peak at Mach {peak_mach}"
784 );
785 assert!(
786 max_value > 0.5 && max_value < 1.0,
787 "G1 peak value: {max_value}"
788 );
789 }
790
791 #[test]
792 fn test_physical_constraints() {
793 let test_machs = [0.1, 0.5, 0.8, 1.0, 1.2, 1.5, 2.0, 3.0, 5.0];
794
795 for &mach in &test_machs {
796 let g1_cd = get_drag_coefficient(mach, &DragModel::G1);
797 let g7_cd = get_drag_coefficient(mach, &DragModel::G7);
798
799 assert!(g1_cd > 0.0, "G1 CD negative at Mach {mach}: {g1_cd}");
801 assert!(g7_cd > 0.0, "G7 CD negative at Mach {mach}: {g7_cd}");
802
803 assert!(g1_cd < 2.0, "G1 CD too high at Mach {mach}: {g1_cd}");
805 assert!(g7_cd < 1.5, "G7 CD too high at Mach {mach}: {g7_cd}");
806 }
807 }
808
809 #[test]
810 fn test_performance_characteristics() {
811 use std::time::Instant;
813
814 let start = Instant::now();
815
816 for i in 0..1000 {
818 let mach = 0.5 + (i as f64) * 0.004; let _g1 = get_drag_coefficient(mach, &DragModel::G1);
820 let _g7 = get_drag_coefficient(mach, &DragModel::G7);
821 }
822
823 let elapsed = start.elapsed();
824
825 assert!(
827 elapsed.as_millis() < 100,
828 "Performance test took {}ms",
829 elapsed.as_millis()
830 );
831 }
832}
833
834pub fn interpolated_bc(mach: f64, segments: &[(f64, f64)]) -> f64 {
836 if segments.is_empty() {
837 return crate::constants::BC_FALLBACK_CONSERVATIVE; }
839
840 let mach_values: Vec<f64> = segments.iter().map(|(m, _)| *m).collect();
842
843 if mach_values.is_empty() || segments.is_empty() {
845 return crate::constants::BC_FALLBACK_CONSERVATIVE; }
847
848 if let Some(first_mach) = mach_values.first() {
850 if mach <= *first_mach {
851 return segments.first().map(|(_, bc)| *bc).unwrap_or(0.5);
852 }
853 }
854
855 if let Some(last_mach) = mach_values.last() {
856 if mach >= *last_mach {
857 return segments.last().map(|(_, bc)| *bc).unwrap_or(0.5);
858 }
859 }
860
861 let idx = match mach_values
863 .binary_search_by(|&m| m.partial_cmp(&mach).unwrap_or(std::cmp::Ordering::Equal))
864 {
865 Ok(idx) => {
866 return segments.get(idx).map(|(_, bc)| *bc).unwrap_or(0.5);
868 }
869 Err(idx) => idx, };
871
872 if idx == 0 || idx >= segments.len() {
874 let safe_idx = idx.saturating_sub(1).min(segments.len().saturating_sub(1));
877 return segments.get(safe_idx).map(|(_, bc)| *bc).unwrap_or(0.5);
878 }
879
880 match (segments.get(idx - 1), segments.get(idx)) {
882 (Some((lo_mach, lo_bc)), Some((hi_mach, hi_bc))) => {
883 let denominator = hi_mach - lo_mach;
885 if denominator.abs() < crate::constants::MIN_DIVISION_THRESHOLD {
886 return *lo_bc; }
888 let frac = (mach - lo_mach) / denominator;
889 lo_bc + frac * (hi_bc - lo_bc)
890 }
891 _ => 0.5, }
893}
894
895