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 fast_integrate(
106 inputs: &BallisticInputs,
107 wind_sock: &WindSock,
108 params: FastIntegrationParams,
109) -> FastSolution {
110 let _mass_kg = inputs.bullet_mass; let bc = inputs.bc_value;
113 let drag_model = &inputs.bc_type;
114
115 let has_bc_segments =
117 inputs.bc_segments.is_some() && !inputs.bc_segments.as_ref().unwrap().is_empty();
118 let has_bc_segments_data =
119 inputs.bc_segments_data.is_some() && !inputs.bc_segments_data.as_ref().unwrap().is_empty();
120
121 let dt = if params.horiz > 200.0 {
123 0.001
124 } else if params.horiz > 100.0 {
125 0.0005
126 } else {
127 0.0001
128 };
129
130 let vx = params.initial_state[3]; let t_max = if vx > 1e-6 && params.horiz > 0.0 {
141 (4.0 * params.horiz / vx).min(params.t_span.1)
142 } else {
143 params.t_span.1
144 };
145
146 let n_steps = ((t_max / dt) as usize) + 1;
148 let mut times = Vec::with_capacity(n_steps);
149 let mut states = Vec::with_capacity(n_steps);
150
151 times.push(0.0);
153 states.push(params.initial_state);
154
155 let base_density = if params.atmo_params.3 > 0.0 {
165 params.atmo_params.3 * 1.225
166 } else {
167 1.225 };
169
170 let drag_model_str: &str = match drag_model {
178 DragModel::G1 => "G1",
179 DragModel::G2 => "G2",
180 DragModel::G5 => "G5",
181 DragModel::G6 => "G6",
182 DragModel::G7 => "G7",
183 DragModel::G8 => "G8",
184 DragModel::GI => "GI",
185 DragModel::GS => "GS",
186 };
187
188 let caliber_in = if inputs.caliber_inches > 0.0 {
190 inputs.caliber_inches
191 } else {
192 inputs.bullet_diameter / 0.0254
193 };
194 let weight_gr = if inputs.weight_grains > 0.0 {
195 inputs.weight_grains
196 } else {
197 inputs.bullet_mass / 0.00006479891
198 };
199
200 let projectile_shape = crate::transonic_drag::resolve_projectile_shape(
203 inputs.bullet_model.as_deref(),
204 caliber_in,
205 weight_gr,
206 drag_model_str,
207 );
208
209 let omega_vector = if inputs.enable_coriolis && inputs.latitude.is_some() {
215 let omega_earth = 7.2921159e-5_f64; let lat = inputs.latitude.unwrap().to_radians();
217 let az = inputs.azimuth_angle; Some(Vector3::new(
219 omega_earth * lat.cos() * az.cos(), omega_earth * lat.sin(), -omega_earth * lat.cos() * az.sin(), ))
223 } else {
224 None
225 };
226
227 let mut hit_target = false;
229 let mut hit_ground = false;
230 let mut max_ord_time = None;
231 let mut max_ord_y = 0.0;
232 let ground_threshold = inputs.ground_threshold;
233
234 for i in 0..n_steps - 1 {
236 let t = i as f64 * dt;
237 let state = states[i];
238
239 let pos = Vector3::new(state[0], state[1], state[2]);
240 let _vel = Vector3::new(state[3], state[4], state[5]);
241
242 if pos.x >= params.horiz {
244 hit_target = true;
245 times.push(t);
246 states.push(state);
247 break;
248 }
249
250 if pos.y <= ground_threshold {
251 hit_ground = true;
252 times.push(t);
253 states.push(state);
254 break;
255 }
256
257 if pos.y > max_ord_y {
259 max_ord_y = pos.y;
260 max_ord_time = Some(t);
261 }
262
263 let k1 = compute_derivatives(
265 &state,
266 inputs,
267 wind_sock,
268 base_density,
269 drag_model,
270 projectile_shape,
271 bc,
272 has_bc_segments,
273 has_bc_segments_data,
274 omega_vector,
275 );
276
277 let mut state2 = state;
278 for j in 0..6 {
279 state2[j] = state[j] + 0.5 * dt * k1[j];
280 }
281 let k2 = compute_derivatives(
282 &state2,
283 inputs,
284 wind_sock,
285 base_density,
286 drag_model,
287 projectile_shape,
288 bc,
289 has_bc_segments,
290 has_bc_segments_data,
291 omega_vector,
292 );
293
294 let mut state3 = state;
295 for j in 0..6 {
296 state3[j] = state[j] + 0.5 * dt * k2[j];
297 }
298 let k3 = compute_derivatives(
299 &state3,
300 inputs,
301 wind_sock,
302 base_density,
303 drag_model,
304 projectile_shape,
305 bc,
306 has_bc_segments,
307 has_bc_segments_data,
308 omega_vector,
309 );
310
311 let mut state4 = state;
312 for j in 0..6 {
313 state4[j] = state[j] + dt * k3[j];
314 }
315 let k4 = compute_derivatives(
316 &state4,
317 inputs,
318 wind_sock,
319 base_density,
320 drag_model,
321 projectile_shape,
322 bc,
323 has_bc_segments,
324 has_bc_segments_data,
325 omega_vector,
326 );
327
328 let mut new_state = state;
330 for j in 0..6 {
331 new_state[j] = state[j] + dt * (k1[j] + 2.0 * k2[j] + 2.0 * k3[j] + k4[j]) / 6.0;
332 }
333
334 times.push(t + dt);
335 states.push(new_state);
336 }
337
338 let t_events = [
340 if hit_target {
341 vec![*times.last().unwrap()]
342 } else {
343 vec![]
344 },
345 if let Some(t) = max_ord_time {
346 vec![t]
347 } else {
348 vec![]
349 },
350 if hit_ground {
351 vec![*times.last().unwrap()]
352 } else {
353 vec![]
354 },
355 ];
356
357 FastSolution::from_trajectory_data(times, states, t_events)
358}
359
360fn compute_derivatives(
362 state: &[f64; 6],
363 inputs: &BallisticInputs,
364 wind_sock: &WindSock,
365 base_density: f64,
366 drag_model: &DragModel,
367 projectile_shape: crate::transonic_drag::ProjectileShape,
368 bc: f64,
369 has_bc_segments: bool,
370 has_bc_segments_data: bool,
371 omega: Option<Vector3<f64>>,
372) -> [f64; 6] {
373 let pos = Vector3::new(state[0], state[1], state[2]);
374 let vel = Vector3::new(state[3], state[4], state[5]);
375
376 let wind_vector = wind_sock.vector_for_range_stateless(pos.x);
378
379 let vel_adjusted = vel - wind_vector;
381 let v_mag = vel_adjusted.norm();
382
383 let mut accel = if v_mag < 1e-6 {
385 Vector3::new(0.0, -G_ACCEL_MPS2, 0.0)
386 } else {
387 let v_fps = v_mag * MPS_TO_FPS;
389
390 let altitude = inputs.altitude + pos.y;
393 let (_, speed_of_sound) = get_local_atmosphere(
394 altitude,
395 inputs.altitude, inputs.temperature,
397 inputs.pressure,
398 if inputs.humidity > 0.0 { inputs.humidity } else { 1.0 },
399 );
400 let mach = v_mag / speed_of_sound;
401
402 let bc_current = if has_bc_segments_data && inputs.bc_segments_data.is_some() {
404 get_bc_from_velocity_segments(v_fps, inputs.bc_segments_data.as_ref().unwrap())
405 } else if has_bc_segments && inputs.bc_segments.is_some() {
406 crate::derivatives::interpolated_bc(
407 mach,
408 inputs.bc_segments.as_ref().unwrap(),
409 Some(inputs),
410 )
411 } else {
412 bc
413 };
414 let bc_current = bc_current.max(1e-6);
417
418 let drag_factor = if let Some(ref table) = inputs.custom_drag_table {
426 table.interpolate(mach)
427 } else {
428 let base_cd = get_drag_coefficient(mach, drag_model);
429 let cd =
430 crate::transonic_drag::transonic_correction(mach, base_cd, projectile_shape, false);
431 crate::form_factor::apply_form_factor_to_drag(
435 cd,
436 inputs.bullet_model.as_deref(),
437 &inputs.bc_type,
438 inputs.use_form_factor,
439 )
440 };
441
442 let cd_to_retard = 0.000683 * 0.30;
444 let standard_factor = drag_factor * cd_to_retard;
445 let density_scale = base_density / 1.225;
446
447 let a_drag_ft_s2 = (v_fps * v_fps) * standard_factor * density_scale / bc_current;
449
450 let a_drag_m_s2 = a_drag_ft_s2 * 0.3048; let accel_drag = -a_drag_m_s2 * (vel_adjusted / v_mag);
453
454 accel_drag + Vector3::new(0.0, -G_ACCEL_MPS2, 0.0)
456 };
457
458 if let Some(omega) = omega {
461 accel += -2.0 * omega.cross(&vel);
462 }
463
464 [vel.x, vel.y, vel.z, accel.x, accel.y, accel.z]
466}
467
468fn get_bc_from_velocity_segments(velocity_fps: f64, segments: &[BCSegmentData]) -> f64 {
470 for segment in segments {
471 if velocity_fps >= segment.velocity_min && velocity_fps <= segment.velocity_max {
472 return segment.bc_value;
473 }
474 }
475
476 if let Some(first) = segments.first() {
478 if velocity_fps < first.velocity_min {
479 return first.bc_value;
480 }
481 }
482
483 if let Some(last) = segments.last() {
484 if velocity_fps > last.velocity_max {
485 return last.bc_value;
486 }
487 }
488
489 0.5
491}
492
493pub fn fast_integrate_with_segments(
496 inputs: &BallisticInputs,
497 wind_segments: Vec<crate::wind::WindSegment>,
498 params: FastIntegrationParams,
499) -> FastSolution {
500 use crate::trajectory_integration::{integrate_trajectory, TrajectoryParams};
502
503 let mass_kg = inputs.bullet_mass; let bc = inputs.bc_value;
506 let drag_model = inputs.bc_type;
507
508 let omega_vector = if inputs.enable_advanced_effects {
510 let omega_earth = 7.2921159e-5; let lat_rad = inputs.latitude.unwrap_or(0.0).to_radians();
516 let azimuth = inputs.azimuth_angle; Some(Vector3::new(
518 omega_earth * lat_rad.cos() * azimuth.cos(), omega_earth * lat_rad.sin(), -omega_earth * lat_rad.cos() * azimuth.sin(), ))
522 } else {
523 None
524 };
525
526 let traj_params = TrajectoryParams {
528 mass_kg,
529 bc,
530 drag_model,
531 wind_segments,
532 atmos_params: params.atmo_params,
533 omega_vector,
534 enable_spin_drift: inputs.enable_advanced_effects,
535 enable_magnus: inputs.enable_advanced_effects,
536 enable_coriolis: inputs.enable_advanced_effects,
537 target_distance_m: params.horiz,
538 enable_wind_shear: inputs.enable_wind_shear,
539 wind_shear_model: inputs.wind_shear_model.clone(),
540 shooter_altitude_m: inputs.altitude,
541 is_twist_right: inputs.is_twist_right,
542 custom_drag_table: inputs.custom_drag_table.clone(),
543 bc_segments: inputs.bc_segments.clone(),
544 use_bc_segments: inputs.use_bc_segments,
545 ground_threshold: -1000.0,
549 };
550
551 let trajectory = integrate_trajectory(
553 params.initial_state,
554 params.t_span,
555 traj_params,
556 "RK45", 1e-6, 0.01, );
560
561 let n_points = trajectory.len();
563 let mut times = Vec::with_capacity(n_points);
564 let mut states = Vec::with_capacity(n_points);
565
566 let mut target_hit_time: Option<f64> = None;
567 let mut ground_hit_time: Option<f64> = None;
568 let mut max_ord_time = None;
569 let mut max_ord_y = 0.0;
570
571 for (t, state_vec) in trajectory {
572 let state = [
574 state_vec[0],
575 state_vec[1],
576 state_vec[2],
577 state_vec[3],
578 state_vec[4],
579 state_vec[5],
580 ];
581
582 if target_hit_time.is_none() && state[0] >= params.horiz {
587 target_hit_time = Some(t);
588 }
589
590 if ground_hit_time.is_none() && state[1] <= inputs.ground_threshold {
592 ground_hit_time = Some(t);
593 }
594
595 if state[1] > max_ord_y {
597 max_ord_y = state[1];
598 max_ord_time = Some(t);
599 }
600
601 times.push(t);
602 states.push(state);
603 }
604
605 let t_events = [
607 if let Some(t) = target_hit_time {
608 vec![t]
609 } else {
610 vec![]
611 },
612 if let Some(t) = max_ord_time {
613 vec![t]
614 } else {
615 vec![]
616 },
617 if let Some(t) = ground_hit_time {
618 vec![t]
619 } else {
620 vec![]
621 },
622 ];
623
624 FastSolution::from_trajectory_data(times, states, t_events)
625}
626
627#[cfg(test)]
628mod tests {
629 use super::*;
630
631 #[test]
632 fn test_fast_solution_interpolation() {
633 let times = vec![0.0, 1.0, 2.0];
634 let states = vec![
635 [0.0, 0.0, 0.0, 100.0, 50.0, 0.0],
636 [100.0, 45.0, 0.0, 99.0, 40.0, 0.0],
637 [198.0, 80.0, 0.0, 98.0, 30.0, 0.0],
638 ];
639
640 let solution = FastSolution::from_trajectory_data(times, states, [vec![], vec![], vec![]]);
641
642 let result = solution.sol(&[1.5]);
644
645 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); }
649
650 #[test]
651 fn test_bc_from_velocity_segments() {
652 let segments = vec![
653 BCSegmentData {
654 velocity_min: 0.0,
655 velocity_max: 1000.0,
656 bc_value: 0.5,
657 },
658 BCSegmentData {
659 velocity_min: 1000.0,
660 velocity_max: 2000.0,
661 bc_value: 0.52,
662 },
663 BCSegmentData {
664 velocity_min: 2000.0,
665 velocity_max: 3000.0,
666 bc_value: 0.55,
667 },
668 ];
669
670 assert_eq!(get_bc_from_velocity_segments(500.0, &segments), 0.5);
671 assert_eq!(get_bc_from_velocity_segments(1500.0, &segments), 0.52);
672 assert_eq!(get_bc_from_velocity_segments(2500.0, &segments), 0.55);
673
674 assert_eq!(get_bc_from_velocity_segments(-100.0, &segments), 0.5); assert_eq!(get_bc_from_velocity_segments(3500.0, &segments), 0.55); }
678
679 #[test]
680 fn test_fast_solution_interpolation_edge_cases() {
681 let times = vec![0.0, 1.0, 2.0, 3.0];
682 let states = vec![
683 [0.0, 0.0, 0.0, 800.0, 50.0, 0.0],
684 [800.0, 40.0, 100.0, 750.0, 30.0, 0.0],
685 [1550.0, 60.0, 200.0, 700.0, 10.0, 0.0],
686 [2250.0, 50.0, 300.0, 650.0, -10.0, 0.0],
687 ];
688
689 let solution = FastSolution::from_trajectory_data(times, states, [vec![], vec![], vec![]]);
690
691 let result_before = solution.sol(&[-0.5]);
693 assert!((result_before[0][0] - 0.0).abs() < 1e-10); let result_after = solution.sol(&[5.0]);
697 assert!((result_after[0][0] - 2250.0).abs() < 1e-10); let result_exact = solution.sol(&[1.0]);
701 assert!((result_exact[0][0] - 800.0).abs() < 1e-10);
702
703 let result_multi = solution.sol(&[0.5, 1.5, 2.5]);
705 assert_eq!(result_multi[0].len(), 3);
706 }
707
708 #[test]
709 fn test_fast_solution_from_trajectory_data() {
710 let times = vec![0.0, 0.5, 1.0];
711 let states = vec![
712 [0.0, 1.0, 2.0, 3.0, 4.0, 5.0],
713 [10.0, 11.0, 12.0, 13.0, 14.0, 15.0],
714 [20.0, 21.0, 22.0, 23.0, 24.0, 25.0],
715 ];
716 let t_events = [vec![1.0], vec![0.5], vec![]];
717
718 let solution = FastSolution::from_trajectory_data(times.clone(), states, t_events);
719
720 assert_eq!(solution.t, times);
722 assert_eq!(solution.y.len(), 6); assert_eq!(solution.y[0].len(), 3); assert!(solution.success);
725
726 assert_eq!(solution.y[0][0], 0.0); assert_eq!(solution.y[1][0], 1.0); assert_eq!(solution.y[0][2], 20.0); }
731
732 #[test]
733 fn test_bc_segments_boundary_conditions() {
734 let single_segment = vec![BCSegmentData {
736 velocity_min: 1000.0,
737 velocity_max: 2000.0,
738 bc_value: 0.5,
739 }];
740
741 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![
748 BCSegmentData {
749 velocity_min: 0.0,
750 velocity_max: 999.0, bc_value: 0.45,
752 },
753 BCSegmentData {
754 velocity_min: 1000.0,
755 velocity_max: 2000.0,
756 bc_value: 0.50,
757 },
758 ];
759
760 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); }
764
765 #[test]
766 fn test_bc_segments_empty_fallback() {
767 let empty_segments: Vec<BCSegmentData> = vec![];
768
769 let result = get_bc_from_velocity_segments(1500.0, &empty_segments);
771 assert_eq!(result, 0.5); }
773
774 #[test]
775 fn test_fast_integration_params() {
776 let params = FastIntegrationParams {
778 horiz: 1000.0,
779 vert: 0.0,
780 initial_state: [0.0, 0.0, 0.0, 800.0, 50.0, 0.0], t_span: (0.0, 5.0),
782 atmo_params: (0.0, 59.0, 29.92, 0.0),
783 };
784
785 assert_eq!(params.horiz, 1000.0);
786 assert_eq!(params.t_span.0, 0.0);
787 assert_eq!(params.t_span.1, 5.0);
788 assert_eq!(params.initial_state[3], 800.0); }
790
791 #[test]
792 fn test_fast_solution_event_arrays() {
793 let times = vec![0.0, 1.0, 2.0];
794 let states = vec![
795 [0.0, 0.0, 0.0, 800.0, 50.0, 0.0],
796 [800.0, 40.0, 500.0, 750.0, 30.0, 0.0],
797 [1500.0, 20.0, 1000.0, 700.0, 10.0, 0.0],
798 ];
799
800 let t_events = [
802 vec![2.0], vec![0.5], vec![], ];
806
807 let solution = FastSolution::from_trajectory_data(times, states, t_events);
808
809 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()); }
813}