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
95pub struct FastIntegrationParams {
97 pub horiz: f64,
98 pub vert: f64,
99 pub initial_state: [f64; 6],
100 pub t_span: (f64, f64),
101 pub atmo_params: (f64, f64, f64, f64),
102}
103
104pub fn aerodynamic_jump_launch_offset_rad(
112 inputs: &BallisticInputs,
113 atmo_params: (f64, f64, f64, f64),
114) -> f64 {
115 if !inputs.enable_aerodynamic_jump {
116 return 0.0;
117 }
118 let diameter = inputs.bullet_diameter;
119 if !(inputs.twist_rate.is_finite() && inputs.twist_rate != 0.0)
120 || !(diameter.is_finite() && diameter > 0.0)
121 || !(inputs.bullet_length.is_finite() && inputs.bullet_length > 0.0)
122 || !inputs.muzzle_velocity.is_finite()
123 {
124 return 0.0;
125 }
126 let sg = crate::stability::compute_stability_coefficient(inputs, atmo_params);
127 if !(sg.is_finite() && sg > 0.0) {
128 return 0.0;
129 }
130 let length_cal = inputs.bullet_length / diameter;
131 const MS_TO_MPH: f64 = 2.236_936_292_054_4;
132 let crosswind_from_right_mph = inputs.wind_speed * inputs.wind_angle.sin() * MS_TO_MPH;
133 let vertical_moa = crate::aerodynamic_jump::litz_crosswind_jump_moa(
134 sg,
135 length_cal,
136 crosswind_from_right_mph,
137 inputs.is_twist_right,
138 );
139 if !vertical_moa.is_finite() {
140 return 0.0;
141 }
142 const MOA_PER_RAD: f64 = 3437.7467707849;
143 vertical_moa / MOA_PER_RAD
144}
145
146fn rotate_launch_velocity(state: &mut [f64; 6], theta_rad: f64) {
149 let (vx, vy, vz) = (state[3], state[4], state[5]);
150 let speed = (vx * vx + vy * vy + vz * vz).sqrt();
151 if speed <= 0.0 {
152 return;
153 }
154 let h = (vx * vx + vz * vz).sqrt(); let new_elev = vy.atan2(h) + theta_rad;
156 state[4] = speed * new_elev.sin();
157 let new_h = speed * new_elev.cos();
158 let scale = if h > 1e-12 { new_h / h } else { 0.0 };
159 state[3] = vx * scale;
160 state[5] = vz * scale;
161}
162
163pub fn fast_integrate(
165 inputs: &BallisticInputs,
166 wind_sock: &WindSock,
167 params: FastIntegrationParams,
168) -> FastSolution {
169 let _mass_kg = inputs.bullet_mass; let bc = inputs.bc_value;
172 let drag_model = &inputs.bc_type;
173
174 let has_bc_segments =
176 inputs.bc_segments.is_some() && !inputs.bc_segments.as_ref().unwrap().is_empty();
177 let has_bc_segments_data =
178 inputs.bc_segments_data.is_some() && !inputs.bc_segments_data.as_ref().unwrap().is_empty();
179
180 let dt = if params.horiz > 200.0 {
182 0.001
183 } else if params.horiz > 100.0 {
184 0.0005
185 } else {
186 0.0001
187 };
188
189 let mut initial_state = params.initial_state;
201 let aj_offset = aerodynamic_jump_launch_offset_rad(inputs, params.atmo_params);
202 if aj_offset != 0.0 {
203 rotate_launch_velocity(&mut initial_state, aj_offset);
204 }
205 let vx = initial_state[3]; let t_max = if vx > 1e-6 && params.horiz > 0.0 {
207 (4.0 * params.horiz / vx).min(params.t_span.1)
208 } else {
209 params.t_span.1
210 };
211
212 let n_steps = ((t_max / dt) as usize) + 1;
214 let mut times = Vec::with_capacity(n_steps);
215 let mut states = Vec::with_capacity(n_steps);
216
217 times.push(0.0);
219 states.push(initial_state);
220
221 let base_density = if params.atmo_params.3 > 0.0 {
231 params.atmo_params.3 * 1.225
232 } else {
233 1.225 };
235
236 let drag_model_str: &str = match drag_model {
244 DragModel::G1 => "G1",
245 DragModel::G2 => "G2",
246 DragModel::G5 => "G5",
247 DragModel::G6 => "G6",
248 DragModel::G7 => "G7",
249 DragModel::G8 => "G8",
250 DragModel::GI => "GI",
251 DragModel::GS => "GS",
252 };
253
254 let caliber_in = if inputs.caliber_inches > 0.0 {
256 inputs.caliber_inches
257 } else {
258 inputs.bullet_diameter / 0.0254
259 };
260 let weight_gr = if inputs.weight_grains > 0.0 {
261 inputs.weight_grains
262 } else {
263 inputs.bullet_mass / 0.00006479891
264 };
265
266 let projectile_shape = crate::transonic_drag::resolve_projectile_shape(
269 inputs.bullet_model.as_deref(),
270 caliber_in,
271 weight_gr,
272 drag_model_str,
273 );
274
275 let omega_vector = if inputs.enable_coriolis && inputs.latitude.is_some() {
281 let omega_earth = 7.2921159e-5_f64; let lat = inputs.latitude.unwrap().to_radians();
283 let az = inputs.azimuth_angle; Some(Vector3::new(
285 omega_earth * lat.cos() * az.cos(), omega_earth * lat.sin(), -omega_earth * lat.cos() * az.sin(), ))
289 } else {
290 None
291 };
292
293 let mut hit_target = false;
295 let mut hit_ground = false;
296 let mut max_ord_time = None;
297 let mut max_ord_y = 0.0;
298 let ground_threshold = inputs.ground_threshold;
299
300 for i in 0..n_steps - 1 {
302 let t = i as f64 * dt;
303 let state = states[i];
304
305 let pos = Vector3::new(state[0], state[1], state[2]);
306 let _vel = Vector3::new(state[3], state[4], state[5]);
307
308 if pos.x >= params.horiz {
310 hit_target = true;
311 times.push(t);
312 states.push(state);
313 break;
314 }
315
316 if pos.y <= ground_threshold {
317 hit_ground = true;
318 times.push(t);
319 states.push(state);
320 break;
321 }
322
323 if pos.y > max_ord_y {
325 max_ord_y = pos.y;
326 max_ord_time = Some(t);
327 }
328
329 let k1 = compute_derivatives(
331 &state,
332 inputs,
333 wind_sock,
334 base_density,
335 drag_model,
336 projectile_shape,
337 bc,
338 has_bc_segments,
339 has_bc_segments_data,
340 omega_vector,
341 );
342
343 let mut state2 = state;
344 for j in 0..6 {
345 state2[j] = state[j] + 0.5 * dt * k1[j];
346 }
347 let k2 = compute_derivatives(
348 &state2,
349 inputs,
350 wind_sock,
351 base_density,
352 drag_model,
353 projectile_shape,
354 bc,
355 has_bc_segments,
356 has_bc_segments_data,
357 omega_vector,
358 );
359
360 let mut state3 = state;
361 for j in 0..6 {
362 state3[j] = state[j] + 0.5 * dt * k2[j];
363 }
364 let k3 = compute_derivatives(
365 &state3,
366 inputs,
367 wind_sock,
368 base_density,
369 drag_model,
370 projectile_shape,
371 bc,
372 has_bc_segments,
373 has_bc_segments_data,
374 omega_vector,
375 );
376
377 let mut state4 = state;
378 for j in 0..6 {
379 state4[j] = state[j] + dt * k3[j];
380 }
381 let k4 = compute_derivatives(
382 &state4,
383 inputs,
384 wind_sock,
385 base_density,
386 drag_model,
387 projectile_shape,
388 bc,
389 has_bc_segments,
390 has_bc_segments_data,
391 omega_vector,
392 );
393
394 let mut new_state = state;
396 for j in 0..6 {
397 new_state[j] = state[j] + dt * (k1[j] + 2.0 * k2[j] + 2.0 * k3[j] + k4[j]) / 6.0;
398 }
399
400 times.push(t + dt);
401 states.push(new_state);
402 }
403
404 let t_events = [
406 if hit_target {
407 vec![*times.last().unwrap()]
408 } else {
409 vec![]
410 },
411 if let Some(t) = max_ord_time {
412 vec![t]
413 } else {
414 vec![]
415 },
416 if hit_ground {
417 vec![*times.last().unwrap()]
418 } else {
419 vec![]
420 },
421 ];
422
423 FastSolution::from_trajectory_data(times, states, t_events)
424}
425
426fn compute_derivatives(
428 state: &[f64; 6],
429 inputs: &BallisticInputs,
430 wind_sock: &WindSock,
431 base_density: f64,
432 drag_model: &DragModel,
433 projectile_shape: crate::transonic_drag::ProjectileShape,
434 bc: f64,
435 has_bc_segments: bool,
436 has_bc_segments_data: bool,
437 omega: Option<Vector3<f64>>,
438) -> [f64; 6] {
439 let pos = Vector3::new(state[0], state[1], state[2]);
440 let vel = Vector3::new(state[3], state[4], state[5]);
441
442 let wind_vector = wind_sock.vector_for_range_stateless(pos.x);
444
445 let vel_adjusted = vel - wind_vector;
447 let v_mag = vel_adjusted.norm();
448
449 let mut accel = if v_mag < 1e-6 {
451 Vector3::new(0.0, -G_ACCEL_MPS2, 0.0)
452 } else {
453 let v_fps = v_mag * MPS_TO_FPS;
455
456 let altitude = inputs.altitude + pos.y;
459 let (_, speed_of_sound) = get_local_atmosphere(
460 altitude,
461 inputs.altitude, inputs.temperature,
463 inputs.pressure,
464 if inputs.humidity > 0.0 { inputs.humidity } else { 1.0 },
465 );
466 let mach = v_mag / speed_of_sound;
467
468 let bc_current = if has_bc_segments_data && inputs.bc_segments_data.is_some() {
470 get_bc_from_velocity_segments(v_fps, inputs.bc_segments_data.as_ref().unwrap())
471 } else if has_bc_segments && inputs.bc_segments.is_some() {
472 crate::derivatives::interpolated_bc(
473 mach,
474 inputs.bc_segments.as_ref().unwrap(),
475 Some(inputs),
476 )
477 } else {
478 bc
479 };
480 let bc_current = bc_current.max(1e-6);
483
484 let drag_factor = if let Some(ref table) = inputs.custom_drag_table {
492 table.interpolate(mach)
493 } else {
494 let base_cd = get_drag_coefficient(mach, drag_model);
495 let cd =
496 crate::transonic_drag::transonic_correction(mach, base_cd, projectile_shape, false);
497 crate::form_factor::apply_form_factor_to_drag(
501 cd,
502 inputs.bullet_model.as_deref(),
503 &inputs.bc_type,
504 inputs.use_form_factor,
505 )
506 };
507
508 let cd_to_retard = 0.000683 * 0.30;
510 let standard_factor = drag_factor * cd_to_retard;
511 let density_scale = base_density / 1.225;
512
513 let a_drag_ft_s2 = (v_fps * v_fps) * standard_factor * density_scale / bc_current;
515
516 let a_drag_m_s2 = a_drag_ft_s2 * 0.3048; let accel_drag = -a_drag_m_s2 * (vel_adjusted / v_mag);
519
520 accel_drag + Vector3::new(0.0, -G_ACCEL_MPS2, 0.0)
522 };
523
524 if let Some(omega) = omega {
527 accel += -2.0 * omega.cross(&vel);
528 }
529
530 [vel.x, vel.y, vel.z, accel.x, accel.y, accel.z]
532}
533
534fn get_bc_from_velocity_segments(velocity_fps: f64, segments: &[BCSegmentData]) -> f64 {
536 for segment in segments {
537 if velocity_fps >= segment.velocity_min && velocity_fps <= segment.velocity_max {
538 return segment.bc_value;
539 }
540 }
541
542 if let Some(first) = segments.first() {
544 if velocity_fps < first.velocity_min {
545 return first.bc_value;
546 }
547 }
548
549 if let Some(last) = segments.last() {
550 if velocity_fps > last.velocity_max {
551 return last.bc_value;
552 }
553 }
554
555 0.5
557}
558
559pub fn fast_integrate_with_segments(
562 inputs: &BallisticInputs,
563 wind_segments: Vec<crate::wind::WindSegment>,
564 params: FastIntegrationParams,
565) -> FastSolution {
566 use crate::trajectory_integration::{integrate_trajectory, TrajectoryParams};
568
569 let mass_kg = inputs.bullet_mass; let bc = inputs.bc_value;
572 let drag_model = inputs.bc_type;
573
574 let omega_vector = if inputs.enable_advanced_effects {
576 let omega_earth = 7.2921159e-5; let lat_rad = inputs.latitude.unwrap_or(0.0).to_radians();
582 let azimuth = inputs.azimuth_angle; Some(Vector3::new(
584 omega_earth * lat_rad.cos() * azimuth.cos(), omega_earth * lat_rad.sin(), -omega_earth * lat_rad.cos() * azimuth.sin(), ))
588 } else {
589 None
590 };
591
592 let traj_params = TrajectoryParams {
594 mass_kg,
595 bc,
596 drag_model,
597 wind_segments,
598 atmos_params: params.atmo_params,
599 omega_vector,
600 enable_spin_drift: inputs.enable_advanced_effects,
601 enable_magnus: inputs.enable_advanced_effects,
602 enable_coriolis: inputs.enable_advanced_effects,
603 target_distance_m: params.horiz,
604 enable_wind_shear: inputs.enable_wind_shear,
605 wind_shear_model: inputs.wind_shear_model.clone(),
606 shooter_altitude_m: inputs.altitude,
607 is_twist_right: inputs.is_twist_right,
608 custom_drag_table: inputs.custom_drag_table.clone(),
609 bc_segments: inputs.bc_segments.clone(),
610 use_bc_segments: inputs.use_bc_segments,
611 ground_threshold: -1000.0,
615 };
616
617 let trajectory = integrate_trajectory(
619 params.initial_state,
620 params.t_span,
621 traj_params,
622 "RK45", 1e-6, 0.01, );
626
627 let n_points = trajectory.len();
629 let mut times = Vec::with_capacity(n_points);
630 let mut states = Vec::with_capacity(n_points);
631
632 let mut target_hit_time: Option<f64> = None;
633 let mut ground_hit_time: Option<f64> = None;
634 let mut max_ord_time = None;
635 let mut max_ord_y = 0.0;
636
637 for (t, state_vec) in trajectory {
638 let state = [
640 state_vec[0],
641 state_vec[1],
642 state_vec[2],
643 state_vec[3],
644 state_vec[4],
645 state_vec[5],
646 ];
647
648 if target_hit_time.is_none() && state[0] >= params.horiz {
653 target_hit_time = Some(t);
654 }
655
656 if ground_hit_time.is_none() && state[1] <= inputs.ground_threshold {
658 ground_hit_time = Some(t);
659 }
660
661 if state[1] > max_ord_y {
663 max_ord_y = state[1];
664 max_ord_time = Some(t);
665 }
666
667 times.push(t);
668 states.push(state);
669 }
670
671 let t_events = [
673 if let Some(t) = target_hit_time {
674 vec![t]
675 } else {
676 vec![]
677 },
678 if let Some(t) = max_ord_time {
679 vec![t]
680 } else {
681 vec![]
682 },
683 if let Some(t) = ground_hit_time {
684 vec![t]
685 } else {
686 vec![]
687 },
688 ];
689
690 FastSolution::from_trajectory_data(times, states, t_events)
691}
692
693#[cfg(test)]
694mod tests {
695 use super::*;
696
697 #[test]
698 fn test_fast_solution_interpolation() {
699 let times = vec![0.0, 1.0, 2.0];
700 let states = vec![
701 [0.0, 0.0, 0.0, 100.0, 50.0, 0.0],
702 [100.0, 45.0, 0.0, 99.0, 40.0, 0.0],
703 [198.0, 80.0, 0.0, 98.0, 30.0, 0.0],
704 ];
705
706 let solution = FastSolution::from_trajectory_data(times, states, [vec![], vec![], vec![]]);
707
708 let result = solution.sol(&[1.5]);
710
711 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); }
715
716 #[test]
717 fn test_bc_from_velocity_segments() {
718 let segments = vec![
719 BCSegmentData {
720 velocity_min: 0.0,
721 velocity_max: 1000.0,
722 bc_value: 0.5,
723 },
724 BCSegmentData {
725 velocity_min: 1000.0,
726 velocity_max: 2000.0,
727 bc_value: 0.52,
728 },
729 BCSegmentData {
730 velocity_min: 2000.0,
731 velocity_max: 3000.0,
732 bc_value: 0.55,
733 },
734 ];
735
736 assert_eq!(get_bc_from_velocity_segments(500.0, &segments), 0.5);
737 assert_eq!(get_bc_from_velocity_segments(1500.0, &segments), 0.52);
738 assert_eq!(get_bc_from_velocity_segments(2500.0, &segments), 0.55);
739
740 assert_eq!(get_bc_from_velocity_segments(-100.0, &segments), 0.5); assert_eq!(get_bc_from_velocity_segments(3500.0, &segments), 0.55); }
744
745 #[test]
746 fn test_fast_solution_interpolation_edge_cases() {
747 let times = vec![0.0, 1.0, 2.0, 3.0];
748 let states = vec![
749 [0.0, 0.0, 0.0, 800.0, 50.0, 0.0],
750 [800.0, 40.0, 100.0, 750.0, 30.0, 0.0],
751 [1550.0, 60.0, 200.0, 700.0, 10.0, 0.0],
752 [2250.0, 50.0, 300.0, 650.0, -10.0, 0.0],
753 ];
754
755 let solution = FastSolution::from_trajectory_data(times, states, [vec![], vec![], vec![]]);
756
757 let result_before = solution.sol(&[-0.5]);
759 assert!((result_before[0][0] - 0.0).abs() < 1e-10); let result_after = solution.sol(&[5.0]);
763 assert!((result_after[0][0] - 2250.0).abs() < 1e-10); let result_exact = solution.sol(&[1.0]);
767 assert!((result_exact[0][0] - 800.0).abs() < 1e-10);
768
769 let result_multi = solution.sol(&[0.5, 1.5, 2.5]);
771 assert_eq!(result_multi[0].len(), 3);
772 }
773
774 #[test]
775 fn test_fast_solution_from_trajectory_data() {
776 let times = vec![0.0, 0.5, 1.0];
777 let states = vec![
778 [0.0, 1.0, 2.0, 3.0, 4.0, 5.0],
779 [10.0, 11.0, 12.0, 13.0, 14.0, 15.0],
780 [20.0, 21.0, 22.0, 23.0, 24.0, 25.0],
781 ];
782 let t_events = [vec![1.0], vec![0.5], vec![]];
783
784 let solution = FastSolution::from_trajectory_data(times.clone(), states, t_events);
785
786 assert_eq!(solution.t, times);
788 assert_eq!(solution.y.len(), 6); assert_eq!(solution.y[0].len(), 3); assert!(solution.success);
791
792 assert_eq!(solution.y[0][0], 0.0); assert_eq!(solution.y[1][0], 1.0); assert_eq!(solution.y[0][2], 20.0); }
797
798 #[test]
799 fn test_bc_segments_boundary_conditions() {
800 let single_segment = vec![BCSegmentData {
802 velocity_min: 1000.0,
803 velocity_max: 2000.0,
804 bc_value: 0.5,
805 }];
806
807 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![
814 BCSegmentData {
815 velocity_min: 0.0,
816 velocity_max: 999.0, bc_value: 0.45,
818 },
819 BCSegmentData {
820 velocity_min: 1000.0,
821 velocity_max: 2000.0,
822 bc_value: 0.50,
823 },
824 ];
825
826 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); }
830
831 #[test]
832 fn test_bc_segments_empty_fallback() {
833 let empty_segments: Vec<BCSegmentData> = vec![];
834
835 let result = get_bc_from_velocity_segments(1500.0, &empty_segments);
837 assert_eq!(result, 0.5); }
839
840 #[test]
841 fn test_fast_integration_params() {
842 let params = FastIntegrationParams {
844 horiz: 1000.0,
845 vert: 0.0,
846 initial_state: [0.0, 0.0, 0.0, 800.0, 50.0, 0.0], t_span: (0.0, 5.0),
848 atmo_params: (0.0, 59.0, 29.92, 0.0),
849 };
850
851 assert_eq!(params.horiz, 1000.0);
852 assert_eq!(params.t_span.0, 0.0);
853 assert_eq!(params.t_span.1, 5.0);
854 assert_eq!(params.initial_state[3], 800.0); }
856
857 #[test]
858 fn test_fast_solution_event_arrays() {
859 let times = vec![0.0, 1.0, 2.0];
860 let states = vec![
861 [0.0, 0.0, 0.0, 800.0, 50.0, 0.0],
862 [800.0, 40.0, 500.0, 750.0, 30.0, 0.0],
863 [1500.0, 20.0, 1000.0, 700.0, 10.0, 0.0],
864 ];
865
866 let t_events = [
868 vec![2.0], vec![0.5], vec![], ];
872
873 let solution = FastSolution::from_trajectory_data(times, states, t_events);
874
875 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()); }
879}