1use crate::{
7 atmosphere::get_local_atmosphere,
8 constants::{G_ACCEL_MPS2, MPS_TO_FPS},
9 drag::get_drag_coefficient,
10 wind::WindSock,
11 BCSegmentData, DragModel, InternalBallisticInputs as BallisticInputs,
12};
13use nalgebra::Vector3;
14
15#[derive(Debug, Clone)]
17pub struct FastSolution {
18 pub t: Vec<f64>,
20 pub y: Vec<Vec<f64>>,
22 pub t_events: [Vec<f64>; 3],
24 pub success: bool,
26}
27
28impl FastSolution {
29 pub fn sol(&self, t_query: &[f64]) -> Vec<Vec<f64>> {
31 let mut result = vec![vec![0.0; t_query.len()]; 6];
32
33 for (i, &tq) in t_query.iter().enumerate() {
34 let idx = match self
37 .t
38 .binary_search_by(|&t| t.partial_cmp(&tq).unwrap_or(std::cmp::Ordering::Greater))
39 {
40 Ok(idx) => idx,
41 Err(idx) => idx,
42 };
43
44 if idx == 0 {
45 for j in 0..6 {
47 result[j][i] = self.y[j][0];
48 }
49 } else if idx >= self.t.len() {
50 for j in 0..6 {
52 result[j][i] = self.y[j][self.t.len() - 1];
53 }
54 } else {
55 let t0 = self.t[idx - 1];
57 let t1 = self.t[idx];
58 let frac = (tq - t0) / (t1 - t0);
59
60 for j in 0..6 {
61 let y0 = self.y[j][idx - 1];
62 let y1 = self.y[j][idx];
63 result[j][i] = y0 + frac * (y1 - y0);
64 }
65 }
66 }
67
68 result
69 }
70
71 pub fn from_trajectory_data(
73 times: Vec<f64>,
74 states: Vec<[f64; 6]>,
75 t_events: [Vec<f64>; 3],
76 ) -> Self {
77 let n_points = times.len();
78 let mut y = vec![vec![0.0; n_points]; 6];
79
80 for (i, state) in states.iter().enumerate() {
81 for j in 0..6 {
82 y[j][i] = state[j];
83 }
84 }
85
86 FastSolution {
87 t: times,
88 y,
89 t_events,
90 success: true,
91 }
92 }
93
94 fn degenerate(initial_state: &[f64; 6]) -> Self {
98 let mut y = vec![Vec::new(); 6];
99 for (j, slot) in y.iter_mut().enumerate() {
100 slot.push(initial_state[j]);
101 }
102 FastSolution {
103 t: vec![0.0],
104 y,
105 t_events: [Vec::new(), Vec::new(), Vec::new()],
106 success: false,
107 }
108 }
109}
110
111fn atmo_is_physical(atmo_params: (f64, f64, f64, f64)) -> bool {
116 let (_alt, temp_c, pressure_hpa, _humidity) = atmo_params;
117 temp_c.is_finite() && pressure_hpa.is_finite() && pressure_hpa > 0.0
118}
119
120pub struct FastIntegrationParams {
122 pub horiz: f64,
123 pub vert: f64,
124 pub initial_state: [f64; 6],
125 pub t_span: (f64, f64),
126 pub atmo_params: (f64, f64, f64, f64),
127}
128
129pub fn aerodynamic_jump_launch_offset_rad(
137 inputs: &BallisticInputs,
138 atmo_params: (f64, f64, f64, f64),
139) -> f64 {
140 if !inputs.enable_aerodynamic_jump {
141 return 0.0;
142 }
143 let diameter = inputs.bullet_diameter;
144 if !(inputs.twist_rate.is_finite() && inputs.twist_rate != 0.0)
145 || !(diameter.is_finite() && diameter > 0.0)
146 || !(inputs.bullet_length.is_finite() && inputs.bullet_length > 0.0)
147 || !inputs.muzzle_velocity.is_finite()
148 {
149 return 0.0;
150 }
151 let sg = crate::stability::compute_stability_coefficient(inputs, atmo_params);
152 if !(sg.is_finite() && sg > 0.0) {
153 return 0.0;
154 }
155 let length_cal = inputs.bullet_length / diameter;
156 const MS_TO_MPH: f64 = 2.236_936_292_054_4;
157 let crosswind_from_right_mph = inputs.wind_speed * inputs.wind_angle.sin() * MS_TO_MPH;
158 let vertical_moa = crate::aerodynamic_jump::litz_crosswind_jump_moa(
159 sg,
160 length_cal,
161 crosswind_from_right_mph,
162 inputs.is_twist_right,
163 );
164 if !vertical_moa.is_finite() {
165 return 0.0;
166 }
167 const MOA_PER_RAD: f64 = 3437.7467707849;
168 vertical_moa / MOA_PER_RAD
169}
170
171fn rotate_launch_velocity(state: &mut [f64; 6], theta_rad: f64) {
174 let (vx, vy, vz) = (state[3], state[4], state[5]);
175 let speed = (vx * vx + vy * vy + vz * vz).sqrt();
176 if speed <= 0.0 {
177 return;
178 }
179 let h = (vx * vx + vz * vz).sqrt(); let new_elev = vy.atan2(h) + theta_rad;
181 state[4] = speed * new_elev.sin();
182 let new_h = speed * new_elev.cos();
183 let scale = if h > 1e-12 { new_h / h } else { 0.0 };
184 state[3] = vx * scale;
185 state[5] = vz * scale;
186}
187
188pub fn fast_integrate(
190 inputs: &BallisticInputs,
191 wind_sock: &WindSock,
192 params: FastIntegrationParams,
193) -> FastSolution {
194 if !atmo_is_physical(params.atmo_params) {
196 return FastSolution::degenerate(¶ms.initial_state);
197 }
198 let _mass_kg = inputs.bullet_mass; let bc = inputs.bc_value;
201 let drag_model = &inputs.bc_type;
202
203 let has_bc_segments =
205 inputs.bc_segments.is_some() && !inputs.bc_segments.as_ref().unwrap().is_empty();
206 let has_bc_segments_data =
207 inputs.bc_segments_data.is_some() && !inputs.bc_segments_data.as_ref().unwrap().is_empty();
208
209 let dt = if params.horiz > 200.0 {
211 0.001
212 } else if params.horiz > 100.0 {
213 0.0005
214 } else {
215 0.0001
216 };
217
218 let mut initial_state = params.initial_state;
230 let aj_offset = aerodynamic_jump_launch_offset_rad(inputs, params.atmo_params);
231 if aj_offset != 0.0 {
232 rotate_launch_velocity(&mut initial_state, aj_offset);
233 }
234 let vx = initial_state[3]; let t_max = if vx > 1e-6 && params.horiz > 0.0 {
236 (4.0 * params.horiz / vx).min(params.t_span.1)
237 } else {
238 params.t_span.1
239 };
240
241 let n_steps = ((t_max / dt) as usize) + 1;
243 let mut times = Vec::with_capacity(n_steps);
244 let mut states = Vec::with_capacity(n_steps);
245
246 times.push(0.0);
248 states.push(initial_state);
249
250 let base_density = if params.atmo_params.3 > 0.0 {
260 params.atmo_params.3 * 1.225
261 } else {
262 1.225 };
264
265 let drag_model_str: &str = match drag_model {
273 DragModel::G1 => "G1",
274 DragModel::G2 => "G2",
275 DragModel::G5 => "G5",
276 DragModel::G6 => "G6",
277 DragModel::G7 => "G7",
278 DragModel::G8 => "G8",
279 DragModel::GI => "GI",
280 DragModel::GS => "GS",
281 };
282
283 let caliber_in = if inputs.caliber_inches > 0.0 {
285 inputs.caliber_inches
286 } else {
287 inputs.bullet_diameter / 0.0254
288 };
289 let weight_gr = if inputs.weight_grains > 0.0 {
290 inputs.weight_grains
291 } else {
292 inputs.bullet_mass / 0.00006479891
293 };
294
295 let projectile_shape = crate::transonic_drag::resolve_projectile_shape(
298 inputs.bullet_model.as_deref(),
299 caliber_in,
300 weight_gr,
301 drag_model_str,
302 );
303
304 let omega_vector = if inputs.enable_coriolis && inputs.latitude.is_some() {
310 let omega_earth = 7.2921159e-5_f64; let lat = inputs.latitude.unwrap().to_radians();
312 let az = inputs.shot_azimuth; Some(Vector3::new(
314 omega_earth * lat.cos() * az.cos(), omega_earth * lat.sin(), -omega_earth * lat.cos() * az.sin(), ))
318 } else {
319 None
320 };
321
322 let mut hit_target = false;
324 let mut hit_ground = false;
325 let mut max_ord_time = None;
326 let mut max_ord_y = 0.0;
327 let ground_threshold = inputs.ground_threshold;
328
329 for i in 0..n_steps - 1 {
331 let t = i as f64 * dt;
332 let state = states[i];
333
334 let pos = Vector3::new(state[0], state[1], state[2]);
335 let _vel = Vector3::new(state[3], state[4], state[5]);
336
337 if pos.x >= params.horiz {
339 hit_target = true;
340 times.push(t);
341 states.push(state);
342 break;
343 }
344
345 if pos.y <= ground_threshold {
346 hit_ground = true;
347 times.push(t);
348 states.push(state);
349 break;
350 }
351
352 if pos.y > max_ord_y {
354 max_ord_y = pos.y;
355 max_ord_time = Some(t);
356 }
357
358 let k1 = compute_derivatives(
360 &state,
361 inputs,
362 wind_sock,
363 base_density,
364 drag_model,
365 projectile_shape,
366 bc,
367 has_bc_segments,
368 has_bc_segments_data,
369 omega_vector,
370 );
371
372 let mut state2 = state;
373 for j in 0..6 {
374 state2[j] = state[j] + 0.5 * dt * k1[j];
375 }
376 let k2 = compute_derivatives(
377 &state2,
378 inputs,
379 wind_sock,
380 base_density,
381 drag_model,
382 projectile_shape,
383 bc,
384 has_bc_segments,
385 has_bc_segments_data,
386 omega_vector,
387 );
388
389 let mut state3 = state;
390 for j in 0..6 {
391 state3[j] = state[j] + 0.5 * dt * k2[j];
392 }
393 let k3 = compute_derivatives(
394 &state3,
395 inputs,
396 wind_sock,
397 base_density,
398 drag_model,
399 projectile_shape,
400 bc,
401 has_bc_segments,
402 has_bc_segments_data,
403 omega_vector,
404 );
405
406 let mut state4 = state;
407 for j in 0..6 {
408 state4[j] = state[j] + dt * k3[j];
409 }
410 let k4 = compute_derivatives(
411 &state4,
412 inputs,
413 wind_sock,
414 base_density,
415 drag_model,
416 projectile_shape,
417 bc,
418 has_bc_segments,
419 has_bc_segments_data,
420 omega_vector,
421 );
422
423 let mut new_state = state;
425 for j in 0..6 {
426 new_state[j] = state[j] + dt * (k1[j] + 2.0 * k2[j] + 2.0 * k3[j] + k4[j]) / 6.0;
427 }
428
429 times.push(t + dt);
430 states.push(new_state);
431 }
432
433 let t_events = [
435 if hit_target {
436 vec![*times.last().unwrap()]
437 } else {
438 vec![]
439 },
440 if let Some(t) = max_ord_time {
441 vec![t]
442 } else {
443 vec![]
444 },
445 if hit_ground {
446 vec![*times.last().unwrap()]
447 } else {
448 vec![]
449 },
450 ];
451
452 FastSolution::from_trajectory_data(times, states, t_events)
453}
454
455fn compute_derivatives(
457 state: &[f64; 6],
458 inputs: &BallisticInputs,
459 wind_sock: &WindSock,
460 base_density: f64,
461 drag_model: &DragModel,
462 projectile_shape: crate::transonic_drag::ProjectileShape,
463 bc: f64,
464 has_bc_segments: bool,
465 has_bc_segments_data: bool,
466 omega: Option<Vector3<f64>>,
467) -> [f64; 6] {
468 let pos = Vector3::new(state[0], state[1], state[2]);
469 let vel = Vector3::new(state[3], state[4], state[5]);
470
471 let wind_vector = wind_sock.vector_for_range_stateless(pos.x);
473
474 let vel_adjusted = vel - wind_vector;
476 let v_mag = vel_adjusted.norm();
477
478 let mut accel = if v_mag < 1e-6 {
480 Vector3::new(0.0, -G_ACCEL_MPS2, 0.0)
481 } else {
482 let v_fps = v_mag * MPS_TO_FPS;
484
485 let altitude = inputs.altitude + pos.y;
488 let (_, speed_of_sound) = get_local_atmosphere(
489 altitude,
490 inputs.altitude, inputs.temperature,
492 inputs.pressure,
493 if inputs.humidity > 0.0 { inputs.humidity } else { 1.0 },
494 );
495 let mach = v_mag / speed_of_sound;
496
497 let bc_current = if has_bc_segments_data && inputs.bc_segments_data.is_some() {
499 get_bc_from_velocity_segments(v_fps, inputs.bc_segments_data.as_ref().unwrap())
500 } else if has_bc_segments && inputs.bc_segments.is_some() {
501 crate::derivatives::interpolated_bc(
502 mach,
503 inputs.bc_segments.as_ref().unwrap(),
504 Some(inputs),
505 )
506 } else {
507 bc
508 };
509 let bc_current = bc_current.max(1e-6);
512
513 let drag_factor = if let Some(ref table) = inputs.custom_drag_table {
521 table.interpolate(mach)
522 } else {
523 let base_cd = get_drag_coefficient(mach, drag_model);
524 let cd =
525 crate::transonic_drag::transonic_correction(mach, base_cd, projectile_shape, false);
526 crate::form_factor::apply_form_factor_to_drag(
530 cd,
531 inputs.bullet_model.as_deref(),
532 &inputs.bc_type,
533 inputs.use_form_factor,
534 )
535 };
536
537 let cd_to_retard = 0.000683 * 0.30;
539 let standard_factor = drag_factor * cd_to_retard;
540 let density_scale = base_density / 1.225;
541
542 let a_drag_ft_s2 = (v_fps * v_fps) * standard_factor * density_scale / bc_current;
544
545 let a_drag_m_s2 = a_drag_ft_s2 * 0.3048; let accel_drag = -a_drag_m_s2 * (vel_adjusted / v_mag);
548
549 accel_drag + Vector3::new(0.0, -G_ACCEL_MPS2, 0.0)
551 };
552
553 if let Some(omega) = omega {
556 accel += -2.0 * omega.cross(&vel);
557 }
558
559 [vel.x, vel.y, vel.z, accel.x, accel.y, accel.z]
561}
562
563fn get_bc_from_velocity_segments(velocity_fps: f64, segments: &[BCSegmentData]) -> f64 {
565 for segment in segments {
566 if velocity_fps >= segment.velocity_min && velocity_fps <= segment.velocity_max {
567 return segment.bc_value;
568 }
569 }
570
571 if let Some(first) = segments.first() {
573 if velocity_fps < first.velocity_min {
574 return first.bc_value;
575 }
576 }
577
578 if let Some(last) = segments.last() {
579 if velocity_fps > last.velocity_max {
580 return last.bc_value;
581 }
582 }
583
584 0.5
586}
587
588pub fn fast_integrate_with_segments(
591 inputs: &BallisticInputs,
592 wind_segments: Vec<crate::wind::WindSegment>,
593 params: FastIntegrationParams,
594) -> FastSolution {
595 use crate::trajectory_integration::{integrate_trajectory, TrajectoryParams};
597
598 if !atmo_is_physical(params.atmo_params) {
600 return FastSolution::degenerate(¶ms.initial_state);
601 }
602
603 let mass_kg = inputs.bullet_mass; let bc = inputs.bc_value;
606 let drag_model = inputs.bc_type;
607
608 let omega_vector = if inputs.enable_coriolis && inputs.latitude.is_some() {
612 let omega_earth = 7.2921159e-5; let lat_rad = inputs.latitude.unwrap_or(0.0).to_radians();
618 let azimuth = inputs.shot_azimuth; Some(Vector3::new(
620 omega_earth * lat_rad.cos() * azimuth.cos(), omega_earth * lat_rad.sin(), -omega_earth * lat_rad.cos() * azimuth.sin(), ))
624 } else {
625 None
626 };
627
628 let traj_params = TrajectoryParams {
630 mass_kg,
631 bc,
632 drag_model,
633 wind_segments,
634 atmos_params: params.atmo_params,
635 omega_vector,
636 enable_spin_drift: inputs.enable_advanced_effects,
637 enable_magnus: inputs.enable_magnus,
638 enable_coriolis: inputs.enable_coriolis,
639 target_distance_m: params.horiz,
640 enable_wind_shear: inputs.enable_wind_shear,
641 wind_shear_model: inputs.wind_shear_model.clone(),
642 shooter_altitude_m: inputs.altitude,
643 is_twist_right: inputs.is_twist_right,
644 custom_drag_table: inputs.custom_drag_table.clone(),
645 bc_segments: inputs.bc_segments.clone(),
646 use_bc_segments: inputs.use_bc_segments,
647 ground_threshold: -1000.0,
651 };
652
653 let trajectory = integrate_trajectory(
655 params.initial_state,
656 params.t_span,
657 traj_params,
658 "RK45", 1e-6, 0.01, );
662
663 let n_points = trajectory.len();
665 let mut times = Vec::with_capacity(n_points);
666 let mut states = Vec::with_capacity(n_points);
667
668 let mut target_hit_time: Option<f64> = None;
669 let mut ground_hit_time: Option<f64> = None;
670 let mut max_ord_time = None;
671 let mut max_ord_y = 0.0;
672
673 for (t, state_vec) in trajectory {
674 let state = [
676 state_vec[0],
677 state_vec[1],
678 state_vec[2],
679 state_vec[3],
680 state_vec[4],
681 state_vec[5],
682 ];
683
684 if target_hit_time.is_none() && state[0] >= params.horiz {
689 target_hit_time = Some(t);
690 }
691
692 if ground_hit_time.is_none() && state[1] <= inputs.ground_threshold {
694 ground_hit_time = Some(t);
695 }
696
697 if state[1] > max_ord_y {
699 max_ord_y = state[1];
700 max_ord_time = Some(t);
701 }
702
703 times.push(t);
704 states.push(state);
705 }
706
707 let t_events = [
709 if let Some(t) = target_hit_time {
710 vec![t]
711 } else {
712 vec![]
713 },
714 if let Some(t) = max_ord_time {
715 vec![t]
716 } else {
717 vec![]
718 },
719 if let Some(t) = ground_hit_time {
720 vec![t]
721 } else {
722 vec![]
723 },
724 ];
725
726 FastSolution::from_trajectory_data(times, states, t_events)
727}
728
729#[cfg(test)]
730mod tests {
731 use super::*;
732
733 #[test]
734 fn test_fast_solution_interpolation() {
735 let times = vec![0.0, 1.0, 2.0];
736 let states = vec![
737 [0.0, 0.0, 0.0, 100.0, 50.0, 0.0],
738 [100.0, 45.0, 0.0, 99.0, 40.0, 0.0],
739 [198.0, 80.0, 0.0, 98.0, 30.0, 0.0],
740 ];
741
742 let solution = FastSolution::from_trajectory_data(times, states, [vec![], vec![], vec![]]);
743
744 let result = solution.sol(&[1.5]);
746
747 assert!((result[0][0] - 149.0).abs() < 1e-10); assert!((result[1][0] - 62.5).abs() < 1e-10); assert!((result[3][0] - 98.5).abs() < 1e-10); }
751
752 #[test]
753 fn test_bc_from_velocity_segments() {
754 let segments = vec![
755 BCSegmentData {
756 velocity_min: 0.0,
757 velocity_max: 1000.0,
758 bc_value: 0.5,
759 },
760 BCSegmentData {
761 velocity_min: 1000.0,
762 velocity_max: 2000.0,
763 bc_value: 0.52,
764 },
765 BCSegmentData {
766 velocity_min: 2000.0,
767 velocity_max: 3000.0,
768 bc_value: 0.55,
769 },
770 ];
771
772 assert_eq!(get_bc_from_velocity_segments(500.0, &segments), 0.5);
773 assert_eq!(get_bc_from_velocity_segments(1500.0, &segments), 0.52);
774 assert_eq!(get_bc_from_velocity_segments(2500.0, &segments), 0.55);
775
776 assert_eq!(get_bc_from_velocity_segments(-100.0, &segments), 0.5); assert_eq!(get_bc_from_velocity_segments(3500.0, &segments), 0.55); }
780
781 #[test]
782 fn test_fast_solution_interpolation_edge_cases() {
783 let times = vec![0.0, 1.0, 2.0, 3.0];
784 let states = vec![
785 [0.0, 0.0, 0.0, 800.0, 50.0, 0.0],
786 [800.0, 40.0, 100.0, 750.0, 30.0, 0.0],
787 [1550.0, 60.0, 200.0, 700.0, 10.0, 0.0],
788 [2250.0, 50.0, 300.0, 650.0, -10.0, 0.0],
789 ];
790
791 let solution = FastSolution::from_trajectory_data(times, states, [vec![], vec![], vec![]]);
792
793 let result_before = solution.sol(&[-0.5]);
795 assert!((result_before[0][0] - 0.0).abs() < 1e-10); let result_after = solution.sol(&[5.0]);
799 assert!((result_after[0][0] - 2250.0).abs() < 1e-10); let result_exact = solution.sol(&[1.0]);
803 assert!((result_exact[0][0] - 800.0).abs() < 1e-10);
804
805 let result_multi = solution.sol(&[0.5, 1.5, 2.5]);
807 assert_eq!(result_multi[0].len(), 3);
808 }
809
810 #[test]
811 fn test_fast_solution_from_trajectory_data() {
812 let times = vec![0.0, 0.5, 1.0];
813 let states = vec![
814 [0.0, 1.0, 2.0, 3.0, 4.0, 5.0],
815 [10.0, 11.0, 12.0, 13.0, 14.0, 15.0],
816 [20.0, 21.0, 22.0, 23.0, 24.0, 25.0],
817 ];
818 let t_events = [vec![1.0], vec![0.5], vec![]];
819
820 let solution = FastSolution::from_trajectory_data(times.clone(), states, t_events);
821
822 assert_eq!(solution.t, times);
824 assert_eq!(solution.y.len(), 6); assert_eq!(solution.y[0].len(), 3); assert!(solution.success);
827
828 assert_eq!(solution.y[0][0], 0.0); assert_eq!(solution.y[1][0], 1.0); assert_eq!(solution.y[0][2], 20.0); }
833
834 #[test]
835 fn test_bc_segments_boundary_conditions() {
836 let single_segment = vec![BCSegmentData {
838 velocity_min: 1000.0,
839 velocity_max: 2000.0,
840 bc_value: 0.5,
841 }];
842
843 assert_eq!(get_bc_from_velocity_segments(500.0, &single_segment), 0.5); assert_eq!(get_bc_from_velocity_segments(1500.0, &single_segment), 0.5); assert_eq!(get_bc_from_velocity_segments(2500.0, &single_segment), 0.5); let segments = vec![
850 BCSegmentData {
851 velocity_min: 0.0,
852 velocity_max: 999.0, bc_value: 0.45,
854 },
855 BCSegmentData {
856 velocity_min: 1000.0,
857 velocity_max: 2000.0,
858 bc_value: 0.50,
859 },
860 ];
861
862 assert_eq!(get_bc_from_velocity_segments(1000.0, &segments), 0.50); assert_eq!(get_bc_from_velocity_segments(0.0, &segments), 0.45); assert_eq!(get_bc_from_velocity_segments(999.0, &segments), 0.45); }
866
867 #[test]
868 fn test_bc_segments_empty_fallback() {
869 let empty_segments: Vec<BCSegmentData> = vec![];
870
871 let result = get_bc_from_velocity_segments(1500.0, &empty_segments);
873 assert_eq!(result, 0.5); }
875
876 #[test]
877 fn test_fast_integration_params() {
878 let params = FastIntegrationParams {
880 horiz: 1000.0,
881 vert: 0.0,
882 initial_state: [0.0, 0.0, 0.0, 800.0, 50.0, 0.0], t_span: (0.0, 5.0),
884 atmo_params: (0.0, 59.0, 29.92, 0.0),
885 };
886
887 assert_eq!(params.horiz, 1000.0);
888 assert_eq!(params.t_span.0, 0.0);
889 assert_eq!(params.t_span.1, 5.0);
890 assert_eq!(params.initial_state[3], 800.0); }
892
893 #[test]
894 fn test_fast_solution_event_arrays() {
895 let times = vec![0.0, 1.0, 2.0];
896 let states = vec![
897 [0.0, 0.0, 0.0, 800.0, 50.0, 0.0],
898 [800.0, 40.0, 500.0, 750.0, 30.0, 0.0],
899 [1500.0, 20.0, 1000.0, 700.0, 10.0, 0.0],
900 ];
901
902 let t_events = [
904 vec![2.0], vec![0.5], vec![], ];
908
909 let solution = FastSolution::from_trajectory_data(times, states, t_events);
910
911 assert_eq!(solution.t_events[0], vec![2.0]); assert_eq!(solution.t_events[1], vec![0.5]); assert!(solution.t_events[2].is_empty()); }
915
916 #[test]
917 fn fast_path_coriolis_uses_shot_direction() {
918 use std::f64::consts::FRAC_PI_2;
923 fn final_xy(shot_az: f64) -> (f64, f64) {
925 let mut inputs = BallisticInputs::default();
926 inputs.muzzle_velocity = 800.0;
927 inputs.bc_value = 0.5;
928 inputs.bc_type = DragModel::G7;
929 inputs.enable_advanced_effects = true; inputs.enable_coriolis = true;
931 inputs.latitude = Some(45.0);
932 inputs.shot_azimuth = shot_az;
933 let v = 800.0_f64;
934 let elev = 0.02_f64;
935 let params = FastIntegrationParams {
936 horiz: 1000.0,
937 vert: 0.0,
938 initial_state: [0.0, 0.0, 0.0, v * elev.cos(), v * elev.sin(), 0.0],
939 t_span: (0.0, 5.0),
940 atmo_params: (0.0, 59.0, 29.92, 0.0),
941 };
942 let sol = fast_integrate_with_segments(&inputs, vec![], params);
943 let n = sol.y[0].len();
944 (sol.y[0][n - 1], sol.y[1][n - 1])
945 }
946 let (ex, ey) = final_xy(FRAC_PI_2); let (wx, wy) = final_xy(3.0 * FRAC_PI_2); assert!(
951 (ex - wx).abs() < 0.5,
952 "east/west downrange should be ~equal (ex={ex:.4}, wx={wx:.4})"
953 );
954 assert!(
957 ey > wy,
958 "fast-path east ({ey:.6}) must be higher than west ({wy:.6}) (Eotvos)"
959 );
960 assert!(
961 (ey - wy) > 1e-5,
962 "fast-path E-W vertical separation ({:.8} m) should be non-zero (the pre-fix bug was exact equality)",
963 ey - wy
964 );
965 }
966
967 #[test]
968 fn fast_path_coriolis_independent_of_advanced_effects() {
969 use std::f64::consts::FRAC_PI_2;
972 fn final_y(coriolis: bool, shot_az: f64) -> f64 {
973 let mut inputs = BallisticInputs::default();
974 inputs.muzzle_velocity = 800.0;
975 inputs.bc_value = 0.5;
976 inputs.bc_type = DragModel::G7;
977 inputs.enable_coriolis = coriolis;
978 inputs.enable_advanced_effects = false; inputs.latitude = Some(45.0);
980 inputs.shot_azimuth = shot_az;
981 let v = 800.0_f64;
982 let elev = 0.02_f64;
983 let params = FastIntegrationParams {
984 horiz: 1000.0,
985 vert: 0.0,
986 initial_state: [0.0, 0.0, 0.0, v * elev.cos(), v * elev.sin(), 0.0],
987 t_span: (0.0, 5.0),
988 atmo_params: (0.0, 59.0, 29.92, 0.0),
989 };
990 let sol = fast_integrate_with_segments(&inputs, vec![], params);
991 let n = sol.y[0].len();
992 sol.y[1][n - 1]
993 }
994 let e = final_y(true, FRAC_PI_2);
996 let w = final_y(true, 3.0 * FRAC_PI_2);
997 assert!(e > w && (e - w) > 1e-5, "Coriolis-only (no advanced effects) must still be directional: E={e} W={w}");
998 let e2 = final_y(false, FRAC_PI_2);
1000 let w2 = final_y(false, 3.0 * FRAC_PI_2);
1001 assert!((e2 - w2).abs() < 1e-9, "with enable_coriolis=false, east/west must be identical: E={e2} W={w2}");
1002 }
1003
1004 #[test]
1005 fn fast_path_rejects_degenerate_atmosphere() {
1006 let mut inputs = BallisticInputs::default();
1007 inputs.muzzle_velocity = 800.0;
1008 inputs.bc_value = 0.5;
1009 inputs.bc_type = DragModel::G7;
1010 let v = 800.0_f64;
1011 let e = 0.02_f64;
1012 let mk = |atmo: (f64, f64, f64, f64)| FastIntegrationParams {
1013 horiz: 500.0,
1014 vert: 0.0,
1015 initial_state: [0.0, 0.0, 0.0, v * e.cos(), v * e.sin(), 0.0],
1016 t_span: (0.0, 5.0),
1017 atmo_params: atmo,
1018 };
1019 let zero_p = fast_integrate_with_segments(&inputs, vec![], mk((0.0, 15.0, 0.0, 50.0)));
1021 assert!(!zero_p.success, "pressure=0 atmosphere must yield success=false");
1022 let nan_p = fast_integrate_with_segments(&inputs, vec![], mk((0.0, 15.0, f64::NAN, 50.0)));
1024 assert!(!nan_p.success, "NaN pressure must yield success=false");
1025 let good = fast_integrate_with_segments(&inputs, vec![], mk((0.0, 15.0, 1013.25, 50.0)));
1027 assert!(good.success, "realistic atmosphere must yield success=true");
1028 }
1029}
1030