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
212fn parse_embedded_drag_table(csv: &str, fallback: &[(f64, f64)]) -> DragTable {
217 let mut mach_values = Vec::new();
218 let mut cd_values = Vec::new();
219 for line in csv.lines() {
220 let line = line.trim();
221 if line.is_empty() {
222 continue;
223 }
224 let mut cols = line.split(',');
225 if let (Some(m), Some(cd)) = (cols.next(), cols.next()) {
226 if let (Ok(m), Ok(cd)) = (m.trim().parse::<f64>(), cd.trim().parse::<f64>()) {
227 mach_values.push(m);
228 cd_values.push(cd);
229 }
230 }
231 }
232 if mach_values.is_empty() {
233 mach_values = fallback.iter().map(|(m, _)| *m).collect();
234 cd_values = fallback.iter().map(|(_, cd)| *cd).collect();
235 }
236 DragTable::new(mach_values, cd_values)
237}
238
239static G1_DRAG_TABLE: LazyLock<DragTable> = LazyLock::new(|| {
244 let fallback_data = [
246 (0.0, 0.2629),
247 (0.5, 0.2695),
248 (0.6, 0.2752),
249 (0.7, 0.2817),
250 (0.8, 0.2902),
251 (0.9, 0.3012),
252 (1.0, 0.4805),
253 (1.1, 0.5933),
254 (1.2, 0.6318),
255 (1.3, 0.6440),
256 (1.4, 0.6444),
257 (1.5, 0.6372),
258 (1.6, 0.6252),
259 (1.7, 0.6105),
260 (1.8, 0.5956),
261 (1.9, 0.5815),
262 (2.0, 0.5934),
263 (2.5, 0.5598),
264 (3.0, 0.5133),
265 (4.0, 0.4811),
266 (5.0, 0.4988),
267 ];
268
269 parse_embedded_drag_table(include_str!("../data/g1.csv"), &fallback_data)
270});
271
272static G7_DRAG_TABLE: LazyLock<DragTable> = LazyLock::new(|| {
277 let fallback_data = [
279 (0.0, 0.1198),
280 (0.5, 0.1197),
281 (0.6, 0.1202),
282 (0.7, 0.1213),
283 (0.8, 0.1240),
284 (0.9, 0.1294),
285 (1.0, 0.3803),
286 (1.1, 0.4015),
287 (1.2, 0.4043),
288 (1.3, 0.3956),
289 (1.4, 0.3814),
290 (1.5, 0.3663),
291 (1.6, 0.3520),
292 (1.7, 0.3398),
293 (1.8, 0.3297),
294 (1.9, 0.3221),
295 (2.0, 0.2980),
296 (2.5, 0.2731),
297 (3.0, 0.2424),
298 (4.0, 0.2196),
299 (5.0, 0.1618),
300 ];
301
302 parse_embedded_drag_table(include_str!("../data/g7.csv"), &fallback_data)
303});
304
305static G6_DRAG_TABLE: LazyLock<DragTable> = LazyLock::new(|| {
308 let fallback_data = [
309 (0.0, 0.2617),
310 (0.05, 0.2553),
311 (0.10, 0.2491),
312 (0.15, 0.2432),
313 (0.20, 0.2376),
314 (0.25, 0.2324),
315 (0.30, 0.2278),
316 (0.35, 0.2238),
317 (0.40, 0.2205),
318 (0.45, 0.2177),
319 (0.50, 0.2155),
320 (0.55, 0.2138),
321 (0.60, 0.2126),
322 (0.65, 0.2121),
323 (0.70, 0.2122),
324 (0.75, 0.2132),
325 (0.80, 0.2154),
326 (0.85, 0.2194),
327 (0.875, 0.2229),
328 (0.90, 0.2297),
329 (0.925, 0.2449),
330 (0.95, 0.2732),
331 (0.975, 0.3141),
332 (1.0, 0.3597),
333 (1.025, 0.3994),
334 (1.05, 0.4261),
335 (1.075, 0.4402),
336 (1.10, 0.4465),
337 (1.125, 0.4490),
338 (1.15, 0.4497),
339 (1.175, 0.4494),
340 (1.20, 0.4482),
341 (1.225, 0.4464),
342 (1.25, 0.4441),
343 (1.30, 0.4390),
344 (1.35, 0.4336),
345 (1.40, 0.4279),
346 (1.45, 0.4221),
347 (1.50, 0.4162),
348 (1.55, 0.4102),
349 (1.60, 0.4042),
350 (1.65, 0.3981),
351 (1.70, 0.3919),
352 (1.75, 0.3855),
353 (1.80, 0.3788),
354 (1.85, 0.3721),
355 (1.90, 0.3652),
356 (1.95, 0.3583),
357 (2.0, 0.3515),
358 (2.05, 0.3447),
359 (2.10, 0.3381),
360 (2.15, 0.3314),
361 (2.20, 0.3249),
362 (2.25, 0.3185),
363 (2.30, 0.3122),
364 (2.35, 0.3060),
365 (2.40, 0.3000),
366 (2.45, 0.2941),
367 (2.50, 0.2883),
368 (2.60, 0.2772),
369 (2.70, 0.2668),
370 (2.80, 0.2574),
371 (2.90, 0.2487),
372 (3.0, 0.2407),
373 (3.10, 0.2333),
374 (3.20, 0.2265),
375 (3.30, 0.2202),
376 (3.40, 0.2144),
377 (3.50, 0.2089),
378 (3.60, 0.2039),
379 (3.70, 0.1991),
380 (3.80, 0.1947),
381 (3.90, 0.1905),
382 (4.0, 0.1866),
383 (4.20, 0.1794),
384 (4.40, 0.1730),
385 (4.60, 0.1673),
386 (4.80, 0.1621),
387 (5.0, 0.1574),
388 ];
389
390 if let Some(drag_dir) = find_drag_tables_dir() {
391 load_drag_table(&drag_dir, "g6", &fallback_data)
392 } else {
393 let mach_values: Vec<f64> = fallback_data.iter().map(|(m, _)| *m).collect();
395 let cd_values: Vec<f64> = fallback_data.iter().map(|(_, cd)| *cd).collect();
396 DragTable::new(mach_values, cd_values)
397 }
398});
399
400static G8_DRAG_TABLE: LazyLock<DragTable> = LazyLock::new(|| {
403 let fallback_data = [
404 (0.0, 0.2105),
405 (0.05, 0.2105),
406 (0.10, 0.2104),
407 (0.15, 0.2104),
408 (0.20, 0.2103),
409 (0.25, 0.2103),
410 (0.30, 0.2103),
411 (0.35, 0.2103),
412 (0.40, 0.2103),
413 (0.45, 0.2102),
414 (0.50, 0.2102),
415 (0.55, 0.2102),
416 (0.60, 0.2102),
417 (0.65, 0.2102),
418 (0.70, 0.2103),
419 (0.75, 0.2103),
420 (0.80, 0.2104),
421 (0.825, 0.2104),
422 (0.85, 0.2105),
423 (0.875, 0.2106),
424 (0.90, 0.2109),
425 (0.925, 0.2183),
426 (0.95, 0.2571),
427 (0.975, 0.3358),
428 (1.0, 0.4068),
429 (1.025, 0.4378),
430 (1.05, 0.4476),
431 (1.075, 0.4493),
432 (1.10, 0.4477),
433 (1.125, 0.4450),
434 (1.15, 0.4419),
435 (1.20, 0.4353),
436 (1.25, 0.4283),
437 (1.30, 0.4208),
438 (1.35, 0.4133),
439 (1.40, 0.4059),
440 (1.45, 0.3986),
441 (1.50, 0.3915),
442 (1.55, 0.3845),
443 (1.60, 0.3777),
444 (1.65, 0.3710),
445 (1.70, 0.3645),
446 (1.75, 0.3581),
447 (1.80, 0.3519),
448 (1.85, 0.3458),
449 (1.90, 0.3400),
450 (1.95, 0.3343),
451 (2.0, 0.3288),
452 (2.05, 0.3234),
453 (2.10, 0.3182),
454 (2.15, 0.3131),
455 (2.20, 0.3081),
456 (2.25, 0.3032),
457 (2.30, 0.2983),
458 (2.35, 0.2937),
459 (2.40, 0.2891),
460 (2.45, 0.2845),
461 (2.50, 0.2802),
462 (2.60, 0.2720),
463 (2.70, 0.2642),
464 (2.80, 0.2569),
465 (2.90, 0.2499),
466 (3.0, 0.2432),
467 (3.10, 0.2368),
468 (3.20, 0.2308),
469 (3.30, 0.2251),
470 (3.40, 0.2197),
471 (3.50, 0.2147),
472 (3.60, 0.2101),
473 (3.70, 0.2058),
474 (3.80, 0.2019),
475 (3.90, 0.1983),
476 (4.0, 0.1950),
477 (4.20, 0.1890),
478 (4.40, 0.1837),
479 (4.60, 0.1791),
480 (4.80, 0.1750),
481 (5.0, 0.1713),
482 ];
483
484 if let Some(drag_dir) = find_drag_tables_dir() {
485 load_drag_table(&drag_dir, "g8", &fallback_data)
486 } else {
487 let mach_values: Vec<f64> = fallback_data.iter().map(|(m, _)| *m).collect();
489 let cd_values: Vec<f64> = fallback_data.iter().map(|(_, cd)| *cd).collect();
490 DragTable::new(mach_values, cd_values)
491 }
492});
493
494pub fn get_drag_coefficient(mach: f64, drag_model: &DragModel) -> f64 {
503 match drag_model {
504 DragModel::G1 => G1_DRAG_TABLE.interpolate(mach),
505 DragModel::G6 => G6_DRAG_TABLE.interpolate(mach),
506 DragModel::G7 => G7_DRAG_TABLE.interpolate(mach),
507 DragModel::G8 => G8_DRAG_TABLE.interpolate(mach),
508 DragModel::G2 | DragModel::G5 | DragModel::GI | DragModel::GS => {
510 G1_DRAG_TABLE.interpolate(mach)
511 }
512 }
513}
514
515pub fn get_drag_coefficient_with_transonic(
517 mach: f64,
518 drag_model: &DragModel,
519 apply_transonic_correction: bool,
520 projectile_shape: Option<ProjectileShape>,
521 caliber: Option<f64>,
522 weight_grains: Option<f64>,
523) -> f64 {
524 let base_cd = get_drag_coefficient(mach, drag_model);
526
527 if apply_transonic_correction && (0.8..=1.3).contains(&mach) {
529 let shape = match projectile_shape {
531 Some(s) => s,
532 None => {
533 if let (Some(cal), Some(weight)) = (caliber, weight_grains) {
534 get_projectile_shape(
535 cal,
536 weight,
537 match drag_model {
538 DragModel::G1 => "G1",
539 DragModel::G6 => "G6",
540 DragModel::G7 => "G7",
541 DragModel::G8 => "G8",
542 _ => "G1", },
544 )
545 } else {
546 ProjectileShape::Spitzer }
548 }
549 };
550
551 transonic_correction(mach, base_cd, shape, true)
553 } else {
554 base_cd
555 }
556}
557
558pub fn get_drag_coefficient_full(
560 mach: f64,
561 drag_model: &DragModel,
562 apply_transonic_correction: bool,
563 apply_reynolds_correction: bool,
564 projectile_shape: Option<ProjectileShape>,
565 caliber: Option<f64>,
566 weight_grains: Option<f64>,
567 velocity_mps: Option<f64>,
568 air_density_kg_m3: Option<f64>,
569 temperature_c: Option<f64>,
570) -> f64 {
571 let mut cd = get_drag_coefficient_with_transonic(
573 mach,
574 drag_model,
575 apply_transonic_correction,
576 projectile_shape,
577 caliber,
578 weight_grains,
579 );
580
581 if apply_reynolds_correction && mach < 1.0 {
583 if let (Some(v), Some(cal), Some(rho), Some(temp)) =
584 (velocity_mps, caliber, air_density_kg_m3, temperature_c)
585 {
586 use crate::reynolds::apply_reynolds_correction;
587 cd = apply_reynolds_correction(cd, v, cal, rho, temp, mach);
588 }
589 }
590
591 cd
592}
593
594#[cfg(test)]
595mod tests {
596 use super::*;
597
598 #[test]
599 fn test_g1_drag_coefficient_interpolation() {
600 let cd = get_drag_coefficient(1.0, &DragModel::G1);
601 assert!(cd > 0.4 && cd < 0.6, "G1 CD at Mach 1.0: {cd}");
603 }
604
605 #[test]
606 fn test_g7_drag_coefficient_interpolation() {
607 let cd = get_drag_coefficient(1.0, &DragModel::G7);
608 assert!(cd > 0.3 && cd < 0.5, "G7 CD at Mach 1.0: {cd}");
610 }
611
612 #[test]
613 fn test_drag_coefficient_continuity() {
614 for mach in [0.5, 0.8, 1.0, 1.2, 1.5, 2.0, 3.0] {
616 let cd_before = get_drag_coefficient(mach - 0.01, &DragModel::G1);
617 let cd_after = get_drag_coefficient(mach + 0.01, &DragModel::G1);
618 let difference = (cd_after - cd_before).abs();
619 assert!(
620 difference < 0.05,
621 "Large discontinuity at Mach {mach}: {cd_before} vs {cd_after}"
622 );
623 }
624 }
625
626 #[test]
627 fn test_extrapolation_bounds() {
628 let cd_low = get_drag_coefficient(0.0, &DragModel::G1);
630 assert!(cd_low > 0.01 && cd_low < 0.5, "Low Mach G1: {cd_low}");
631
632 let cd_high = get_drag_coefficient(10.0, &DragModel::G1);
634 assert!(cd_high > 0.01, "High Mach G1 should be positive: {cd_high}");
635
636 let cd_low_g7 = get_drag_coefficient(0.0, &DragModel::G7);
638 assert!(
639 cd_low_g7 > 0.01,
640 "Low Mach G7 should be positive: {cd_low_g7}"
641 );
642
643 let cd_high_g7 = get_drag_coefficient(20.0, &DragModel::G7);
644 assert!(
645 cd_high_g7 >= 0.01,
646 "High Mach G7 should be positive: {cd_high_g7}"
647 );
648 }
649
650 #[test]
651 fn test_drag_table_creation() {
652 let mach_vals = vec![0.5, 1.0, 1.5, 2.0];
653 let cd_vals = vec![0.2, 0.5, 0.4, 0.3];
654 let table = DragTable::new(mach_vals, cd_vals);
655
656 assert!((table.interpolate(1.0) - 0.5).abs() < 1e-10);
658
659 let cd_interp = table.interpolate(1.25);
661 assert!(cd_interp > 0.4 && cd_interp < 0.5);
662 }
663
664 #[test]
665 fn test_drag_table_empty() {
666 let table = DragTable::new(vec![], vec![]);
667 let result = table.interpolate(1.0);
668 assert_eq!(result, 0.5); }
670
671 #[test]
672 fn test_drag_table_single_point() {
673 let table = DragTable::new(vec![1.0], vec![0.4]);
674
675 assert_eq!(table.interpolate(0.5), 0.4);
677 assert_eq!(table.interpolate(1.0), 0.4);
678 assert_eq!(table.interpolate(2.0), 0.4);
679 }
680
681 #[test]
682 fn test_drag_table_two_points() {
683 let table = DragTable::new(vec![1.0, 2.0], vec![0.4, 0.6]);
684
685 assert!((table.interpolate(1.0) - 0.4).abs() < 1e-10);
687 assert!((table.interpolate(2.0) - 0.6).abs() < 1e-10);
688
689 let mid = table.interpolate(1.5);
691 assert!((mid - 0.5).abs() < 1e-10);
692
693 let below = table.interpolate(0.5);
695 assert!(below.abs() > 1e-10); let above = table.interpolate(3.0);
698 assert!(above.abs() > 1e-10); }
700
701 #[test]
702 fn test_linear_interpolation() {
703 let table = DragTable::new(vec![0.0, 1.0, 2.0], vec![0.2, 0.5, 0.3]);
704
705 let result = table.linear_interpolate(0.5, 0);
707 assert!((result - 0.35).abs() < 1e-10);
708
709 let table_same = DragTable::new(vec![1.0, 1.0], vec![0.4, 0.6]);
711 let result_same = table_same.linear_interpolate(1.0, 0);
712 assert_eq!(result_same, 0.4); }
714
715 #[test]
716 fn test_cubic_interpolation() {
717 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]);
719
720 let result = table.cubic_interpolate(1.25, 1);
722
723 assert!(result > 0.3 && result < 0.7);
725
726 let linear_result = table.linear_interpolate(1.25, 1);
728 assert!((result - linear_result).abs() < 0.2);
730 }
731
732 #[test]
733 fn test_find_drag_tables_dir() {
734 let _dir = find_drag_tables_dir();
737 }
739
740 #[test]
741 fn test_load_drag_table_fallback() {
742 use std::path::Path;
743
744 let fake_dir = Path::new("/non/existent/directory");
746 let fallback_data = [(0.5, 0.2), (1.0, 0.4), (1.5, 0.3)];
747
748 let table = load_drag_table(fake_dir, "test", &fallback_data);
749
750 assert_eq!(table.mach_values.len(), 3);
752 assert_eq!(table.cd_values.len(), 3);
753 assert_eq!(table.mach_values[0], 0.5);
754 assert_eq!(table.cd_values[0], 0.2);
755 }
756
757 #[test]
758 fn test_known_drag_values() {
759 let g1_mach1 = get_drag_coefficient(1.0, &DragModel::G1);
763 assert!(
764 (g1_mach1 - 0.4805).abs() < 0.01,
765 "G1 at Mach 1.0: {g1_mach1}"
766 );
767
768 let g7_mach1 = get_drag_coefficient(1.0, &DragModel::G7);
770 assert!(
771 (g7_mach1 - 0.3803).abs() < 0.01,
772 "G7 at Mach 1.0: {g7_mach1}"
773 );
774
775 assert!(g1_mach1 > g7_mach1, "G1 should be > G7 at Mach 1.0");
777 }
778
779 #[test]
780 fn test_monotonicity_properties() {
781 let mach_values: Vec<f64> = (8..20).map(|i| i as f64 * 0.1).collect(); let g1_values: Vec<f64> = mach_values
786 .iter()
787 .map(|&m| get_drag_coefficient(m, &DragModel::G1))
788 .collect();
789
790 let max_value = g1_values.iter().copied().fold(0.0_f64, f64::max);
792 let max_index = g1_values
793 .iter()
794 .position(|&x| x == max_value)
795 .expect("Should find maximum in non-empty vector");
796 let peak_mach = mach_values
797 .get(max_index)
798 .copied()
799 .expect("Index should be valid");
800
801 assert!(
803 peak_mach > 1.0 && peak_mach < 1.6,
804 "G1 peak at Mach {peak_mach}"
805 );
806 assert!(
807 max_value > 0.5 && max_value < 1.0,
808 "G1 peak value: {max_value}"
809 );
810 }
811
812 #[test]
813 fn test_physical_constraints() {
814 let test_machs = [0.1, 0.5, 0.8, 1.0, 1.2, 1.5, 2.0, 3.0, 5.0];
815
816 for &mach in &test_machs {
817 let g1_cd = get_drag_coefficient(mach, &DragModel::G1);
818 let g7_cd = get_drag_coefficient(mach, &DragModel::G7);
819
820 assert!(g1_cd > 0.0, "G1 CD negative at Mach {mach}: {g1_cd}");
822 assert!(g7_cd > 0.0, "G7 CD negative at Mach {mach}: {g7_cd}");
823
824 assert!(g1_cd < 2.0, "G1 CD too high at Mach {mach}: {g1_cd}");
826 assert!(g7_cd < 1.5, "G7 CD too high at Mach {mach}: {g7_cd}");
827 }
828 }
829
830 #[test]
831 fn test_performance_characteristics() {
832 use std::time::Instant;
834
835 let start = Instant::now();
836
837 for i in 0..1000 {
839 let mach = 0.5 + (i as f64) * 0.004; let _g1 = get_drag_coefficient(mach, &DragModel::G1);
841 let _g7 = get_drag_coefficient(mach, &DragModel::G7);
842 }
843
844 let elapsed = start.elapsed();
845
846 assert!(
848 elapsed.as_millis() < 100,
849 "Performance test took {}ms",
850 elapsed.as_millis()
851 );
852 }
853}
854
855pub fn interpolated_bc(mach: f64, segments: &[(f64, f64)]) -> f64 {
857 if segments.is_empty() {
858 return crate::constants::BC_FALLBACK_CONSERVATIVE; }
860
861 let mach_values: Vec<f64> = segments.iter().map(|(m, _)| *m).collect();
863
864 if mach_values.is_empty() || segments.is_empty() {
866 return crate::constants::BC_FALLBACK_CONSERVATIVE; }
868
869 if let Some(first_mach) = mach_values.first() {
871 if mach <= *first_mach {
872 return segments.first().map(|(_, bc)| *bc).unwrap_or(0.5);
873 }
874 }
875
876 if let Some(last_mach) = mach_values.last() {
877 if mach >= *last_mach {
878 return segments.last().map(|(_, bc)| *bc).unwrap_or(0.5);
879 }
880 }
881
882 let idx = match mach_values
884 .binary_search_by(|&m| m.partial_cmp(&mach).unwrap_or(std::cmp::Ordering::Equal))
885 {
886 Ok(idx) => {
887 return segments.get(idx).map(|(_, bc)| *bc).unwrap_or(0.5);
889 }
890 Err(idx) => idx, };
892
893 if idx == 0 || idx >= segments.len() {
895 let safe_idx = idx.saturating_sub(1).min(segments.len().saturating_sub(1));
898 return segments.get(safe_idx).map(|(_, bc)| *bc).unwrap_or(0.5);
899 }
900
901 match (segments.get(idx - 1), segments.get(idx)) {
903 (Some((lo_mach, lo_bc)), Some((hi_mach, hi_bc))) => {
904 let denominator = hi_mach - lo_mach;
906 if denominator.abs() < crate::constants::MIN_DIVISION_THRESHOLD {
907 return *lo_bc; }
909 let frac = (mach - lo_mach) / denominator;
910 lo_bc + frac * (hi_bc - lo_bc)
911 }
912 _ => 0.5, }
914}
915
916