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 {
124 let (a, b, c, d) = atmo_params;
125 if !(a.is_finite() && b.is_finite() && c.is_finite() && d.is_finite()) {
126 return false;
127 }
128 let direct_mode = c == 0.0 && d == 0.0 && a > 0.0 && a < 2.0 && b > 200.0;
131 direct_mode || c > 0.0
133}
134
135pub struct FastIntegrationParams {
137 pub horiz: f64,
138 pub vert: f64,
139 pub initial_state: [f64; 6],
140 pub t_span: (f64, f64),
141 pub atmo_params: (f64, f64, f64, f64),
145}
146
147pub fn aerodynamic_jump_launch_offset_rad(
155 inputs: &BallisticInputs,
156 atmo_params: (f64, f64, f64, f64),
157) -> f64 {
158 if !inputs.enable_aerodynamic_jump {
159 return 0.0;
160 }
161 let diameter = inputs.bullet_diameter;
162 if !(inputs.twist_rate.is_finite() && inputs.twist_rate != 0.0)
163 || !(diameter.is_finite() && diameter > 0.0)
164 || !(inputs.bullet_length.is_finite() && inputs.bullet_length > 0.0)
165 || !inputs.muzzle_velocity.is_finite()
166 {
167 return 0.0;
168 }
169 let sg = crate::stability::compute_stability_coefficient(inputs, atmo_params);
170 if !(sg.is_finite() && sg > 0.0) {
171 return 0.0;
172 }
173 let length_cal = inputs.bullet_length / diameter;
174 const MS_TO_MPH: f64 = 2.236_936_292_054_4;
175 let crosswind_from_right_mph = inputs.wind_speed * inputs.wind_angle.sin() * MS_TO_MPH;
176 let vertical_moa = crate::aerodynamic_jump::litz_crosswind_jump_moa(
177 sg,
178 length_cal,
179 crosswind_from_right_mph,
180 inputs.is_twist_right,
181 );
182 if !vertical_moa.is_finite() {
183 return 0.0;
184 }
185 const MOA_PER_RAD: f64 = 3437.7467707849;
186 vertical_moa / MOA_PER_RAD
187}
188
189fn rotate_launch_velocity(state: &mut [f64; 6], theta_rad: f64) {
192 let (vx, vy, vz) = (state[3], state[4], state[5]);
193 let speed = (vx * vx + vy * vy + vz * vz).sqrt();
194 if speed <= 0.0 {
195 return;
196 }
197 let h = (vx * vx + vz * vz).sqrt(); let new_elev = vy.atan2(h) + theta_rad;
199 state[4] = speed * new_elev.sin();
200 let new_h = speed * new_elev.cos();
201 let scale = if h > 1e-12 { new_h / h } else { 0.0 };
202 state[3] = vx * scale;
203 state[5] = vz * scale;
204}
205
206pub fn fast_integrate(
208 inputs: &BallisticInputs,
209 wind_sock: &WindSock,
210 params: FastIntegrationParams,
211) -> FastSolution {
212 if !atmo_is_physical(params.atmo_params) {
214 return FastSolution::degenerate(¶ms.initial_state);
215 }
216 let _mass_kg = inputs.bullet_mass; let bc = inputs.bc_value;
219 let drag_model = &inputs.bc_type;
220
221 let has_bc_segments =
223 inputs.bc_segments.is_some() && !inputs.bc_segments.as_ref().unwrap().is_empty();
224 let has_bc_segments_data =
225 inputs.bc_segments_data.is_some() && !inputs.bc_segments_data.as_ref().unwrap().is_empty();
226
227 let dt = if params.horiz > 200.0 {
229 0.001
230 } else if params.horiz > 100.0 {
231 0.0005
232 } else {
233 0.0001
234 };
235
236 let mut initial_state = params.initial_state;
248 let aj_offset = aerodynamic_jump_launch_offset_rad(inputs, params.atmo_params);
249 if aj_offset != 0.0 {
250 rotate_launch_velocity(&mut initial_state, aj_offset);
251 }
252 let vx = initial_state[3]; let t_max = if vx > 1e-6 && params.horiz > 0.0 {
254 (4.0 * params.horiz / vx).min(params.t_span.1)
255 } else {
256 params.t_span.1
257 };
258
259 let n_steps = ((t_max / dt) as usize) + 1;
261 let mut times = Vec::with_capacity(n_steps);
262 let mut states = Vec::with_capacity(n_steps);
263
264 times.push(0.0);
266 states.push(initial_state);
267
268 let base_density = if params.atmo_params.3 > 0.0 {
278 params.atmo_params.3 * 1.225
279 } else {
280 1.225 };
282
283 let drag_model_str: &str = match drag_model {
291 DragModel::G1 => "G1",
292 DragModel::G2 => "G2",
293 DragModel::G5 => "G5",
294 DragModel::G6 => "G6",
295 DragModel::G7 => "G7",
296 DragModel::G8 => "G8",
297 DragModel::GI => "GI",
298 DragModel::GS => "GS",
299 };
300
301 let caliber_in = if inputs.caliber_inches > 0.0 {
303 inputs.caliber_inches
304 } else {
305 inputs.bullet_diameter / 0.0254
306 };
307 let weight_gr = if inputs.weight_grains > 0.0 {
308 inputs.weight_grains
309 } else {
310 inputs.bullet_mass / 0.00006479891
311 };
312
313 let projectile_shape = crate::transonic_drag::resolve_projectile_shape(
316 inputs.bullet_model.as_deref(),
317 caliber_in,
318 weight_gr,
319 drag_model_str,
320 );
321
322 let omega_vector = if inputs.enable_coriolis && inputs.latitude.is_some() {
328 let omega_earth = 7.2921159e-5_f64; let lat = inputs.latitude.unwrap().to_radians();
330 let az = inputs.shot_azimuth; Some(Vector3::new(
332 omega_earth * lat.cos() * az.cos(), omega_earth * lat.sin(), -omega_earth * lat.cos() * az.sin(), ))
336 } else {
337 None
338 };
339
340 let mut hit_target = false;
342 let mut hit_ground = false;
343 let mut max_ord_time = None;
344 let mut max_ord_y = 0.0;
345 let ground_threshold = inputs.ground_threshold;
346
347 for i in 0..n_steps - 1 {
349 let t = i as f64 * dt;
350 let state = states[i];
351
352 let pos = Vector3::new(state[0], state[1], state[2]);
353 let _vel = Vector3::new(state[3], state[4], state[5]);
354
355 if pos.x >= params.horiz {
357 hit_target = true;
358 times.push(t);
359 states.push(state);
360 break;
361 }
362
363 if pos.y <= ground_threshold {
364 hit_ground = true;
365 times.push(t);
366 states.push(state);
367 break;
368 }
369
370 if pos.y > max_ord_y {
372 max_ord_y = pos.y;
373 max_ord_time = Some(t);
374 }
375
376 let k1 = compute_derivatives(
378 &state,
379 inputs,
380 wind_sock,
381 base_density,
382 drag_model,
383 projectile_shape,
384 bc,
385 has_bc_segments,
386 has_bc_segments_data,
387 omega_vector,
388 );
389
390 let mut state2 = state;
391 for j in 0..6 {
392 state2[j] = state[j] + 0.5 * dt * k1[j];
393 }
394 let k2 = compute_derivatives(
395 &state2,
396 inputs,
397 wind_sock,
398 base_density,
399 drag_model,
400 projectile_shape,
401 bc,
402 has_bc_segments,
403 has_bc_segments_data,
404 omega_vector,
405 );
406
407 let mut state3 = state;
408 for j in 0..6 {
409 state3[j] = state[j] + 0.5 * dt * k2[j];
410 }
411 let k3 = compute_derivatives(
412 &state3,
413 inputs,
414 wind_sock,
415 base_density,
416 drag_model,
417 projectile_shape,
418 bc,
419 has_bc_segments,
420 has_bc_segments_data,
421 omega_vector,
422 );
423
424 let mut state4 = state;
425 for j in 0..6 {
426 state4[j] = state[j] + dt * k3[j];
427 }
428 let k4 = compute_derivatives(
429 &state4,
430 inputs,
431 wind_sock,
432 base_density,
433 drag_model,
434 projectile_shape,
435 bc,
436 has_bc_segments,
437 has_bc_segments_data,
438 omega_vector,
439 );
440
441 let mut new_state = state;
443 for j in 0..6 {
444 new_state[j] = state[j] + dt * (k1[j] + 2.0 * k2[j] + 2.0 * k3[j] + k4[j]) / 6.0;
445 }
446
447 times.push(t + dt);
448 states.push(new_state);
449 }
450
451 let t_events = [
453 if hit_target {
454 vec![*times.last().unwrap()]
455 } else {
456 vec![]
457 },
458 if let Some(t) = max_ord_time {
459 vec![t]
460 } else {
461 vec![]
462 },
463 if hit_ground {
464 vec![*times.last().unwrap()]
465 } else {
466 vec![]
467 },
468 ];
469
470 FastSolution::from_trajectory_data(times, states, t_events)
471}
472
473fn compute_derivatives(
475 state: &[f64; 6],
476 inputs: &BallisticInputs,
477 wind_sock: &WindSock,
478 base_density: f64,
479 drag_model: &DragModel,
480 projectile_shape: crate::transonic_drag::ProjectileShape,
481 bc: f64,
482 has_bc_segments: bool,
483 has_bc_segments_data: bool,
484 omega: Option<Vector3<f64>>,
485) -> [f64; 6] {
486 let pos = Vector3::new(state[0], state[1], state[2]);
487 let vel = Vector3::new(state[3], state[4], state[5]);
488
489 let wind_vector = wind_sock.vector_for_range_stateless(pos.x);
491
492 let vel_adjusted = vel - wind_vector;
494 let v_mag = vel_adjusted.norm();
495
496 let mut accel = if v_mag < 1e-6 {
498 Vector3::new(0.0, -G_ACCEL_MPS2, 0.0)
499 } else {
500 let v_fps = v_mag * MPS_TO_FPS;
502
503 let altitude = inputs.altitude + pos.y;
506 let (_, speed_of_sound) = get_local_atmosphere(
507 altitude,
508 inputs.altitude, inputs.temperature,
510 inputs.pressure,
511 if inputs.humidity > 0.0 { inputs.humidity } else { 1.0 },
512 );
513 let mach = v_mag / speed_of_sound;
514
515 let bc_current = if has_bc_segments_data && inputs.bc_segments_data.is_some() {
517 get_bc_from_velocity_segments(v_fps, inputs.bc_segments_data.as_ref().unwrap())
518 } else if has_bc_segments && inputs.bc_segments.is_some() {
519 crate::derivatives::interpolated_bc(
520 mach,
521 inputs.bc_segments.as_ref().unwrap(),
522 Some(inputs),
523 )
524 } else {
525 bc
526 };
527 let bc_current = bc_current.max(1e-6);
530
531 let drag_factor = if let Some(ref table) = inputs.custom_drag_table {
539 table.interpolate(mach)
540 } else {
541 let base_cd = get_drag_coefficient(mach, drag_model);
542 let cd =
543 crate::transonic_drag::transonic_correction(mach, base_cd, projectile_shape, false);
544 crate::form_factor::apply_form_factor_to_drag(
548 cd,
549 inputs.bullet_model.as_deref(),
550 &inputs.bc_type,
551 inputs.use_form_factor,
552 )
553 };
554
555 let cd_to_retard = 0.000683 * 0.30;
557 let standard_factor = drag_factor * cd_to_retard;
558 let density_scale = base_density / 1.225;
559
560 let a_drag_ft_s2 = (v_fps * v_fps) * standard_factor * density_scale / bc_current;
562
563 let a_drag_m_s2 = a_drag_ft_s2 * 0.3048; let accel_drag = -a_drag_m_s2 * (vel_adjusted / v_mag);
566
567 accel_drag + Vector3::new(0.0, -G_ACCEL_MPS2, 0.0)
569 };
570
571 if let Some(omega) = omega {
574 accel += -2.0 * omega.cross(&vel);
575 }
576
577 [vel.x, vel.y, vel.z, accel.x, accel.y, accel.z]
579}
580
581fn get_bc_from_velocity_segments(velocity_fps: f64, segments: &[BCSegmentData]) -> f64 {
583 for segment in segments {
584 if velocity_fps >= segment.velocity_min && velocity_fps <= segment.velocity_max {
585 return segment.bc_value;
586 }
587 }
588
589 if let Some(first) = segments.first() {
591 if velocity_fps < first.velocity_min {
592 return first.bc_value;
593 }
594 }
595
596 if let Some(last) = segments.last() {
597 if velocity_fps > last.velocity_max {
598 return last.bc_value;
599 }
600 }
601
602 0.5
604}
605
606pub fn fast_integrate_with_segments(
609 inputs: &BallisticInputs,
610 wind_segments: Vec<crate::wind::WindSegment>,
611 params: FastIntegrationParams,
612) -> FastSolution {
613 use crate::trajectory_integration::{integrate_trajectory, TrajectoryParams};
615
616 if !atmo_is_physical(params.atmo_params) {
618 return FastSolution::degenerate(¶ms.initial_state);
619 }
620
621 let mass_kg = inputs.bullet_mass; let bc = inputs.bc_value;
624 let drag_model = inputs.bc_type;
625
626 let omega_vector = if inputs.enable_coriolis && inputs.latitude.is_some() {
630 let omega_earth = 7.2921159e-5; let lat_rad = inputs.latitude.unwrap_or(0.0).to_radians();
636 let azimuth = inputs.shot_azimuth; Some(Vector3::new(
638 omega_earth * lat_rad.cos() * azimuth.cos(), omega_earth * lat_rad.sin(), -omega_earth * lat_rad.cos() * azimuth.sin(), ))
642 } else {
643 None
644 };
645
646 let traj_params = TrajectoryParams {
648 mass_kg,
649 bc,
650 drag_model,
651 wind_segments,
652 atmos_params: params.atmo_params,
653 omega_vector,
654 enable_spin_drift: inputs.enable_advanced_effects,
655 enable_magnus: inputs.enable_magnus,
656 enable_coriolis: inputs.enable_coriolis,
657 target_distance_m: params.horiz,
658 enable_wind_shear: inputs.enable_wind_shear,
659 wind_shear_model: inputs.wind_shear_model.clone(),
660 shooter_altitude_m: inputs.altitude,
661 is_twist_right: inputs.is_twist_right,
662 bullet_diameter: inputs.bullet_diameter,
664 bullet_length: inputs.bullet_length,
665 twist_rate: inputs.twist_rate,
666 custom_drag_table: inputs.custom_drag_table.clone(),
667 bc_segments: inputs.bc_segments.clone(),
668 use_bc_segments: inputs.use_bc_segments,
669 ground_threshold: -1000.0,
673 };
674
675 let trajectory = integrate_trajectory(
677 params.initial_state,
678 params.t_span,
679 traj_params,
680 "RK45", 1e-6, 0.01, );
684
685 let n_points = trajectory.len();
687 let mut times = Vec::with_capacity(n_points);
688 let mut states = Vec::with_capacity(n_points);
689
690 let mut target_hit_time: Option<f64> = None;
691 let mut ground_hit_time: Option<f64> = None;
692 let mut max_ord_time = None;
693 let mut max_ord_y = 0.0;
694
695 for (t, state_vec) in trajectory {
696 let state = [
698 state_vec[0],
699 state_vec[1],
700 state_vec[2],
701 state_vec[3],
702 state_vec[4],
703 state_vec[5],
704 ];
705
706 if target_hit_time.is_none() && state[0] >= params.horiz {
711 target_hit_time = Some(t);
712 }
713
714 if ground_hit_time.is_none() && state[1] <= inputs.ground_threshold {
716 ground_hit_time = Some(t);
717 }
718
719 if state[1] > max_ord_y {
721 max_ord_y = state[1];
722 max_ord_time = Some(t);
723 }
724
725 times.push(t);
726 states.push(state);
727 }
728
729 let t_events = [
731 if let Some(t) = target_hit_time {
732 vec![t]
733 } else {
734 vec![]
735 },
736 if let Some(t) = max_ord_time {
737 vec![t]
738 } else {
739 vec![]
740 },
741 if let Some(t) = ground_hit_time {
742 vec![t]
743 } else {
744 vec![]
745 },
746 ];
747
748 FastSolution::from_trajectory_data(times, states, t_events)
749}
750
751#[cfg(test)]
752mod tests {
753 use super::*;
754
755 #[test]
756 fn test_fast_solution_interpolation() {
757 let times = vec![0.0, 1.0, 2.0];
758 let states = vec![
759 [0.0, 0.0, 0.0, 100.0, 50.0, 0.0],
760 [100.0, 45.0, 0.0, 99.0, 40.0, 0.0],
761 [198.0, 80.0, 0.0, 98.0, 30.0, 0.0],
762 ];
763
764 let solution = FastSolution::from_trajectory_data(times, states, [vec![], vec![], vec![]]);
765
766 let result = solution.sol(&[1.5]);
768
769 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); }
773
774 #[test]
775 fn test_bc_from_velocity_segments() {
776 let segments = vec![
777 BCSegmentData {
778 velocity_min: 0.0,
779 velocity_max: 1000.0,
780 bc_value: 0.5,
781 },
782 BCSegmentData {
783 velocity_min: 1000.0,
784 velocity_max: 2000.0,
785 bc_value: 0.52,
786 },
787 BCSegmentData {
788 velocity_min: 2000.0,
789 velocity_max: 3000.0,
790 bc_value: 0.55,
791 },
792 ];
793
794 assert_eq!(get_bc_from_velocity_segments(500.0, &segments), 0.5);
795 assert_eq!(get_bc_from_velocity_segments(1500.0, &segments), 0.52);
796 assert_eq!(get_bc_from_velocity_segments(2500.0, &segments), 0.55);
797
798 assert_eq!(get_bc_from_velocity_segments(-100.0, &segments), 0.5); assert_eq!(get_bc_from_velocity_segments(3500.0, &segments), 0.55); }
802
803 #[test]
804 fn test_fast_solution_interpolation_edge_cases() {
805 let times = vec![0.0, 1.0, 2.0, 3.0];
806 let states = vec![
807 [0.0, 0.0, 0.0, 800.0, 50.0, 0.0],
808 [800.0, 40.0, 100.0, 750.0, 30.0, 0.0],
809 [1550.0, 60.0, 200.0, 700.0, 10.0, 0.0],
810 [2250.0, 50.0, 300.0, 650.0, -10.0, 0.0],
811 ];
812
813 let solution = FastSolution::from_trajectory_data(times, states, [vec![], vec![], vec![]]);
814
815 let result_before = solution.sol(&[-0.5]);
817 assert!((result_before[0][0] - 0.0).abs() < 1e-10); let result_after = solution.sol(&[5.0]);
821 assert!((result_after[0][0] - 2250.0).abs() < 1e-10); let result_exact = solution.sol(&[1.0]);
825 assert!((result_exact[0][0] - 800.0).abs() < 1e-10);
826
827 let result_multi = solution.sol(&[0.5, 1.5, 2.5]);
829 assert_eq!(result_multi[0].len(), 3);
830 }
831
832 #[test]
833 fn test_fast_solution_from_trajectory_data() {
834 let times = vec![0.0, 0.5, 1.0];
835 let states = vec![
836 [0.0, 1.0, 2.0, 3.0, 4.0, 5.0],
837 [10.0, 11.0, 12.0, 13.0, 14.0, 15.0],
838 [20.0, 21.0, 22.0, 23.0, 24.0, 25.0],
839 ];
840 let t_events = [vec![1.0], vec![0.5], vec![]];
841
842 let solution = FastSolution::from_trajectory_data(times.clone(), states, t_events);
843
844 assert_eq!(solution.t, times);
846 assert_eq!(solution.y.len(), 6); assert_eq!(solution.y[0].len(), 3); assert!(solution.success);
849
850 assert_eq!(solution.y[0][0], 0.0); assert_eq!(solution.y[1][0], 1.0); assert_eq!(solution.y[0][2], 20.0); }
855
856 #[test]
857 fn test_bc_segments_boundary_conditions() {
858 let single_segment = vec![BCSegmentData {
860 velocity_min: 1000.0,
861 velocity_max: 2000.0,
862 bc_value: 0.5,
863 }];
864
865 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![
872 BCSegmentData {
873 velocity_min: 0.0,
874 velocity_max: 999.0, bc_value: 0.45,
876 },
877 BCSegmentData {
878 velocity_min: 1000.0,
879 velocity_max: 2000.0,
880 bc_value: 0.50,
881 },
882 ];
883
884 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); }
888
889 #[test]
890 fn test_bc_segments_empty_fallback() {
891 let empty_segments: Vec<BCSegmentData> = vec![];
892
893 let result = get_bc_from_velocity_segments(1500.0, &empty_segments);
895 assert_eq!(result, 0.5); }
897
898 #[test]
899 fn test_fast_integration_params() {
900 let params = FastIntegrationParams {
902 horiz: 1000.0,
903 vert: 0.0,
904 initial_state: [0.0, 0.0, 0.0, 800.0, 50.0, 0.0], t_span: (0.0, 5.0),
906 atmo_params: (0.0, 59.0, 29.92, 0.0),
907 };
908
909 assert_eq!(params.horiz, 1000.0);
910 assert_eq!(params.t_span.0, 0.0);
911 assert_eq!(params.t_span.1, 5.0);
912 assert_eq!(params.initial_state[3], 800.0); }
914
915 #[test]
916 fn test_fast_solution_event_arrays() {
917 let times = vec![0.0, 1.0, 2.0];
918 let states = vec![
919 [0.0, 0.0, 0.0, 800.0, 50.0, 0.0],
920 [800.0, 40.0, 500.0, 750.0, 30.0, 0.0],
921 [1500.0, 20.0, 1000.0, 700.0, 10.0, 0.0],
922 ];
923
924 let t_events = [
926 vec![2.0], vec![0.5], vec![], ];
930
931 let solution = FastSolution::from_trajectory_data(times, states, t_events);
932
933 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()); }
937
938 #[test]
939 fn fast_path_coriolis_uses_shot_direction() {
940 use std::f64::consts::FRAC_PI_2;
945 fn final_xy(shot_az: f64) -> (f64, f64) {
947 let mut inputs = BallisticInputs::default();
948 inputs.muzzle_velocity = 800.0;
949 inputs.bc_value = 0.5;
950 inputs.bc_type = DragModel::G7;
951 inputs.enable_advanced_effects = true; inputs.enable_coriolis = true;
953 inputs.latitude = Some(45.0);
954 inputs.shot_azimuth = shot_az;
955 let v = 800.0_f64;
956 let elev = 0.02_f64;
957 let params = FastIntegrationParams {
958 horiz: 1000.0,
959 vert: 0.0,
960 initial_state: [0.0, 0.0, 0.0, v * elev.cos(), v * elev.sin(), 0.0],
961 t_span: (0.0, 5.0),
962 atmo_params: (0.0, 59.0, 29.92, 0.0),
963 };
964 let sol = fast_integrate_with_segments(&inputs, vec![], params);
965 let n = sol.y[0].len();
966 (sol.y[0][n - 1], sol.y[1][n - 1])
967 }
968 let (ex, ey) = final_xy(FRAC_PI_2); let (wx, wy) = final_xy(3.0 * FRAC_PI_2); assert!(
973 (ex - wx).abs() < 0.5,
974 "east/west downrange should be ~equal (ex={ex:.4}, wx={wx:.4})"
975 );
976 assert!(
979 ey > wy,
980 "fast-path east ({ey:.6}) must be higher than west ({wy:.6}) (Eotvos)"
981 );
982 assert!(
983 (ey - wy) > 1e-5,
984 "fast-path E-W vertical separation ({:.8} m) should be non-zero (the pre-fix bug was exact equality)",
985 ey - wy
986 );
987 }
988
989 #[test]
990 fn fast_path_coriolis_independent_of_advanced_effects() {
991 use std::f64::consts::FRAC_PI_2;
994 fn final_y(coriolis: bool, shot_az: f64) -> f64 {
995 let mut inputs = BallisticInputs::default();
996 inputs.muzzle_velocity = 800.0;
997 inputs.bc_value = 0.5;
998 inputs.bc_type = DragModel::G7;
999 inputs.enable_coriolis = coriolis;
1000 inputs.enable_advanced_effects = false; inputs.latitude = Some(45.0);
1002 inputs.shot_azimuth = shot_az;
1003 let v = 800.0_f64;
1004 let elev = 0.02_f64;
1005 let params = FastIntegrationParams {
1006 horiz: 1000.0,
1007 vert: 0.0,
1008 initial_state: [0.0, 0.0, 0.0, v * elev.cos(), v * elev.sin(), 0.0],
1009 t_span: (0.0, 5.0),
1010 atmo_params: (0.0, 59.0, 29.92, 0.0),
1011 };
1012 let sol = fast_integrate_with_segments(&inputs, vec![], params);
1013 let n = sol.y[0].len();
1014 sol.y[1][n - 1]
1015 }
1016 let e = final_y(true, FRAC_PI_2);
1018 let w = final_y(true, 3.0 * FRAC_PI_2);
1019 assert!(e > w && (e - w) > 1e-5, "Coriolis-only (no advanced effects) must still be directional: E={e} W={w}");
1020 let e2 = final_y(false, FRAC_PI_2);
1022 let w2 = final_y(false, 3.0 * FRAC_PI_2);
1023 assert!((e2 - w2).abs() < 1e-9, "with enable_coriolis=false, east/west must be identical: E={e2} W={w2}");
1024 }
1025
1026 #[test]
1027 fn fast_path_rejects_degenerate_atmosphere() {
1028 let mut inputs = BallisticInputs::default();
1029 inputs.muzzle_velocity = 800.0;
1030 inputs.bc_value = 0.5;
1031 inputs.bc_type = DragModel::G7;
1032 let v = 800.0_f64;
1033 let e = 0.02_f64;
1034 let mk = |atmo: (f64, f64, f64, f64)| FastIntegrationParams {
1035 horiz: 500.0,
1036 vert: 0.0,
1037 initial_state: [0.0, 0.0, 0.0, v * e.cos(), v * e.sin(), 0.0],
1038 t_span: (0.0, 5.0),
1039 atmo_params: atmo,
1040 };
1041 let zero_p = fast_integrate_with_segments(&inputs, vec![], mk((0.0, 15.0, 0.0, 50.0)));
1043 assert!(!zero_p.success, "pressure=0 atmosphere must yield success=false");
1044 let nan_p = fast_integrate_with_segments(&inputs, vec![], mk((0.0, 15.0, f64::NAN, 50.0)));
1046 assert!(!nan_p.success, "NaN pressure must yield success=false");
1047 let good = fast_integrate_with_segments(&inputs, vec![], mk((0.0, 15.0, 1013.25, 50.0)));
1049 assert!(good.success, "realistic atmosphere must yield success=true");
1050 let direct = fast_integrate_with_segments(&inputs, vec![], mk((1.225, 340.0, 0.0, 0.0)));
1053 assert!(direct.success, "direct-atmosphere mode (pressure=0 sentinel) must yield success=true");
1054 }
1055
1056 #[test]
1057 fn fast_path_carries_real_bullet_geometry() {
1058 let run = |diameter: f64, twist: f64| {
1066 let mut inputs = BallisticInputs::default();
1067 inputs.muzzle_velocity = 800.0;
1068 inputs.bc_value = 0.5;
1069 inputs.bc_type = DragModel::G7;
1070 inputs.bullet_diameter = diameter;
1071 inputs.bullet_length = 0.0318;
1072 inputs.twist_rate = twist;
1073 inputs.enable_advanced_effects = true;
1074 inputs.enable_magnus = true;
1075 let v = 800.0_f64;
1076 let elev = 0.02_f64;
1077 let params = FastIntegrationParams {
1078 horiz: 1000.0,
1079 vert: 0.0,
1080 initial_state: [0.0, 0.0, 0.0, v * elev.cos(), v * elev.sin(), 0.0],
1081 t_span: (0.0, 5.0),
1082 atmo_params: (0.0, 15.0, 1013.25, 50.0),
1083 };
1084 fast_integrate_with_segments(&inputs, vec![], params)
1085 };
1086 assert!(run(0.00569, 7.0).success, ".224 geometry must solve");
1089 assert!(run(0.00858, 10.0).success, ".338 geometry must solve");
1090 }
1091}
1092