1use crate::{
7 atmosphere::get_local_atmosphere,
8 constants::{GRAINS_TO_KG, 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 * GRAINS_TO_KG;
112 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 v0 = Vector3::new(
132 params.initial_state[3],
133 params.initial_state[4],
134 params.initial_state[5],
135 )
136 .norm();
137
138 let t_max = if v0 > 1e-6 && params.horiz > 0.0 {
139 (2.0 * params.horiz / v0).min(params.t_span.1)
140 } else {
141 params.t_span.1
142 };
143
144 let n_steps = ((t_max / dt) as usize) + 1;
146 let mut times = Vec::with_capacity(n_steps);
147 let mut states = Vec::with_capacity(n_steps);
148
149 times.push(0.0);
151 states.push(params.initial_state);
152
153 let (base_density, _) = get_local_atmosphere(
155 0.0,
156 params.atmo_params.0,
157 params.atmo_params.1,
158 params.atmo_params.2,
159 params.atmo_params.3,
160 );
161
162 let mut hit_target = false;
164 let mut hit_ground = false;
165 let mut max_ord_time = None;
166 let mut max_ord_y = 0.0;
167 let ground_threshold = inputs.ground_threshold;
168
169 for i in 0..n_steps - 1 {
171 let t = i as f64 * dt;
172 let state = states[i];
173
174 let pos = Vector3::new(state[0], state[1], state[2]);
175 let _vel = Vector3::new(state[3], state[4], state[5]);
176
177 if pos.z >= params.horiz {
179 hit_target = true;
180 times.push(t);
181 states.push(state);
182 break;
183 }
184
185 if pos.y <= ground_threshold {
186 hit_ground = true;
187 times.push(t);
188 states.push(state);
189 break;
190 }
191
192 if pos.y > max_ord_y {
194 max_ord_y = pos.y;
195 max_ord_time = Some(t);
196 }
197
198 let k1 = compute_derivatives(
200 &state,
201 inputs,
202 wind_sock,
203 base_density,
204 drag_model,
205 bc,
206 has_bc_segments,
207 has_bc_segments_data,
208 );
209
210 let mut state2 = state;
211 for j in 0..6 {
212 state2[j] = state[j] + 0.5 * dt * k1[j];
213 }
214 let k2 = compute_derivatives(
215 &state2,
216 inputs,
217 wind_sock,
218 base_density,
219 drag_model,
220 bc,
221 has_bc_segments,
222 has_bc_segments_data,
223 );
224
225 let mut state3 = state;
226 for j in 0..6 {
227 state3[j] = state[j] + 0.5 * dt * k2[j];
228 }
229 let k3 = compute_derivatives(
230 &state3,
231 inputs,
232 wind_sock,
233 base_density,
234 drag_model,
235 bc,
236 has_bc_segments,
237 has_bc_segments_data,
238 );
239
240 let mut state4 = state;
241 for j in 0..6 {
242 state4[j] = state[j] + dt * k3[j];
243 }
244 let k4 = compute_derivatives(
245 &state4,
246 inputs,
247 wind_sock,
248 base_density,
249 drag_model,
250 bc,
251 has_bc_segments,
252 has_bc_segments_data,
253 );
254
255 let mut new_state = state;
257 for j in 0..6 {
258 new_state[j] = state[j] + dt * (k1[j] + 2.0 * k2[j] + 2.0 * k3[j] + k4[j]) / 6.0;
259 }
260
261 times.push(t + dt);
262 states.push(new_state);
263 }
264
265 let t_events = [
267 if hit_target {
268 vec![*times.last().unwrap()]
269 } else {
270 vec![]
271 },
272 if let Some(t) = max_ord_time {
273 vec![t]
274 } else {
275 vec![]
276 },
277 if hit_ground {
278 vec![*times.last().unwrap()]
279 } else {
280 vec![]
281 },
282 ];
283
284 FastSolution::from_trajectory_data(times, states, t_events)
285}
286
287fn compute_derivatives(
289 state: &[f64; 6],
290 inputs: &BallisticInputs,
291 wind_sock: &WindSock,
292 base_density: f64,
293 drag_model: &DragModel,
294 bc: f64,
295 has_bc_segments: bool,
296 has_bc_segments_data: bool,
297) -> [f64; 6] {
298 let pos = Vector3::new(state[0], state[1], state[2]);
299 let vel = Vector3::new(state[3], state[4], state[5]);
300
301 let wind_vector = wind_sock.vector_for_range_stateless(pos.z);
303
304 let vel_adjusted = vel - wind_vector;
306 let v_mag = vel_adjusted.norm();
307
308 let accel = if v_mag < 1e-6 {
310 Vector3::new(0.0, -G_ACCEL_MPS2, 0.0)
311 } else {
312 let v_fps = v_mag * MPS_TO_FPS;
314
315 let altitude = inputs.altitude + pos.y;
318 let (_, speed_of_sound) = get_local_atmosphere(
319 altitude,
320 inputs.altitude, inputs.temperature,
322 inputs.pressure,
323 if inputs.humidity > 0.0 { inputs.humidity } else { 1.0 },
324 );
325 let mach = v_mag / speed_of_sound;
326
327 let bc_current = if has_bc_segments_data && inputs.bc_segments_data.is_some() {
329 get_bc_from_velocity_segments(v_fps, inputs.bc_segments_data.as_ref().unwrap())
330 } else if has_bc_segments && inputs.bc_segments.is_some() {
331 crate::derivatives::interpolated_bc(
332 mach,
333 inputs.bc_segments.as_ref().unwrap(),
334 Some(inputs),
335 )
336 } else {
337 bc
338 };
339
340 let drag_factor = get_drag_coefficient(mach, drag_model);
341
342 let cd_to_retard = 0.000683 * 0.30;
344 let standard_factor = drag_factor * cd_to_retard;
345 let density_scale = base_density / 1.225;
346
347 let a_drag_ft_s2 = (v_fps * v_fps) * standard_factor * density_scale / bc_current;
349
350 let a_drag_m_s2 = a_drag_ft_s2 * 0.3048; let accel_drag = -a_drag_m_s2 * (vel_adjusted / v_mag);
353
354 accel_drag + Vector3::new(0.0, -G_ACCEL_MPS2, 0.0)
356 };
357
358 [vel.x, vel.y, vel.z, accel.x, accel.y, accel.z]
360}
361
362fn get_bc_from_velocity_segments(velocity_fps: f64, segments: &[BCSegmentData]) -> f64 {
364 for segment in segments {
365 if velocity_fps >= segment.velocity_min && velocity_fps <= segment.velocity_max {
366 return segment.bc_value;
367 }
368 }
369
370 if let Some(first) = segments.first() {
372 if velocity_fps < first.velocity_min {
373 return first.bc_value;
374 }
375 }
376
377 if let Some(last) = segments.last() {
378 if velocity_fps > last.velocity_max {
379 return last.bc_value;
380 }
381 }
382
383 0.5
385}
386
387pub fn fast_integrate_with_segments(
390 inputs: &BallisticInputs,
391 wind_segments: Vec<crate::wind::WindSegment>,
392 params: FastIntegrationParams,
393) -> FastSolution {
394 use crate::trajectory_integration::{integrate_trajectory, TrajectoryParams};
396
397 let mass_kg = inputs.bullet_mass * GRAINS_TO_KG;
399 let bc = inputs.bc_value;
400 let drag_model = inputs.bc_type;
401
402 let omega_vector = if inputs.enable_advanced_effects {
404 let omega_earth = 7.2921159e-5; let lat_rad = inputs.latitude.unwrap_or(0.0).to_radians();
410 let azimuth = inputs.azimuth_angle; Some(Vector3::new(
412 omega_earth * lat_rad.cos() * azimuth.sin(),
413 omega_earth * lat_rad.sin(),
414 omega_earth * lat_rad.cos() * azimuth.cos(),
415 ))
416 } else {
417 None
418 };
419
420 let traj_params = TrajectoryParams {
422 mass_kg,
423 bc,
424 drag_model,
425 wind_segments,
426 atmos_params: params.atmo_params,
427 omega_vector,
428 enable_spin_drift: inputs.enable_advanced_effects,
429 enable_magnus: inputs.enable_advanced_effects,
430 enable_coriolis: inputs.enable_advanced_effects,
431 target_distance_m: params.horiz,
432 enable_wind_shear: inputs.enable_wind_shear,
433 wind_shear_model: inputs.wind_shear_model.clone(),
434 shooter_altitude_m: inputs.altitude,
435 is_twist_right: inputs.is_twist_right,
436 custom_drag_table: inputs.custom_drag_table.clone(),
437 bc_segments: inputs.bc_segments.clone(),
438 use_bc_segments: inputs.use_bc_segments,
439 };
440
441 let trajectory = integrate_trajectory(
443 params.initial_state,
444 params.t_span,
445 traj_params,
446 "RK45", 1e-6, 0.01, );
450
451 let n_points = trajectory.len();
453 let mut times = Vec::with_capacity(n_points);
454 let mut states = Vec::with_capacity(n_points);
455
456 let mut target_hit_time: Option<f64> = None;
457 let mut ground_hit_time: Option<f64> = None;
458 let mut max_ord_time = None;
459 let mut max_ord_y = 0.0;
460
461 for (t, state_vec) in trajectory {
462 let state = [
464 state_vec[0],
465 state_vec[1],
466 state_vec[2],
467 state_vec[3],
468 state_vec[4],
469 state_vec[5],
470 ];
471
472 if target_hit_time.is_none() && state[2] >= params.horiz {
477 target_hit_time = Some(t);
478 }
479
480 if ground_hit_time.is_none() && state[1] <= inputs.ground_threshold {
482 ground_hit_time = Some(t);
483 }
484
485 if state[1] > max_ord_y {
487 max_ord_y = state[1];
488 max_ord_time = Some(t);
489 }
490
491 times.push(t);
492 states.push(state);
493 }
494
495 let t_events = [
497 if let Some(t) = target_hit_time {
498 vec![t]
499 } else {
500 vec![]
501 },
502 if let Some(t) = max_ord_time {
503 vec![t]
504 } else {
505 vec![]
506 },
507 if let Some(t) = ground_hit_time {
508 vec![t]
509 } else {
510 vec![]
511 },
512 ];
513
514 FastSolution::from_trajectory_data(times, states, t_events)
515}
516
517#[cfg(test)]
518mod tests {
519 use super::*;
520
521 #[test]
522 fn test_fast_solution_interpolation() {
523 let times = vec![0.0, 1.0, 2.0];
524 let states = vec![
525 [0.0, 0.0, 0.0, 100.0, 50.0, 0.0],
526 [100.0, 45.0, 0.0, 99.0, 40.0, 0.0],
527 [198.0, 80.0, 0.0, 98.0, 30.0, 0.0],
528 ];
529
530 let solution = FastSolution::from_trajectory_data(times, states, [vec![], vec![], vec![]]);
531
532 let result = solution.sol(&[1.5]);
534
535 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); }
539
540 #[test]
541 fn test_bc_from_velocity_segments() {
542 let segments = vec![
543 BCSegmentData {
544 velocity_min: 0.0,
545 velocity_max: 1000.0,
546 bc_value: 0.5,
547 },
548 BCSegmentData {
549 velocity_min: 1000.0,
550 velocity_max: 2000.0,
551 bc_value: 0.52,
552 },
553 BCSegmentData {
554 velocity_min: 2000.0,
555 velocity_max: 3000.0,
556 bc_value: 0.55,
557 },
558 ];
559
560 assert_eq!(get_bc_from_velocity_segments(500.0, &segments), 0.5);
561 assert_eq!(get_bc_from_velocity_segments(1500.0, &segments), 0.52);
562 assert_eq!(get_bc_from_velocity_segments(2500.0, &segments), 0.55);
563
564 assert_eq!(get_bc_from_velocity_segments(-100.0, &segments), 0.5); assert_eq!(get_bc_from_velocity_segments(3500.0, &segments), 0.55); }
568
569 #[test]
570 fn test_fast_solution_interpolation_edge_cases() {
571 let times = vec![0.0, 1.0, 2.0, 3.0];
572 let states = vec![
573 [0.0, 0.0, 0.0, 800.0, 50.0, 0.0],
574 [800.0, 40.0, 100.0, 750.0, 30.0, 0.0],
575 [1550.0, 60.0, 200.0, 700.0, 10.0, 0.0],
576 [2250.0, 50.0, 300.0, 650.0, -10.0, 0.0],
577 ];
578
579 let solution = FastSolution::from_trajectory_data(times, states, [vec![], vec![], vec![]]);
580
581 let result_before = solution.sol(&[-0.5]);
583 assert!((result_before[0][0] - 0.0).abs() < 1e-10); let result_after = solution.sol(&[5.0]);
587 assert!((result_after[0][0] - 2250.0).abs() < 1e-10); let result_exact = solution.sol(&[1.0]);
591 assert!((result_exact[0][0] - 800.0).abs() < 1e-10);
592
593 let result_multi = solution.sol(&[0.5, 1.5, 2.5]);
595 assert_eq!(result_multi[0].len(), 3);
596 }
597
598 #[test]
599 fn test_fast_solution_from_trajectory_data() {
600 let times = vec![0.0, 0.5, 1.0];
601 let states = vec![
602 [0.0, 1.0, 2.0, 3.0, 4.0, 5.0],
603 [10.0, 11.0, 12.0, 13.0, 14.0, 15.0],
604 [20.0, 21.0, 22.0, 23.0, 24.0, 25.0],
605 ];
606 let t_events = [vec![1.0], vec![0.5], vec![]];
607
608 let solution = FastSolution::from_trajectory_data(times.clone(), states, t_events);
609
610 assert_eq!(solution.t, times);
612 assert_eq!(solution.y.len(), 6); assert_eq!(solution.y[0].len(), 3); assert!(solution.success);
615
616 assert_eq!(solution.y[0][0], 0.0); assert_eq!(solution.y[1][0], 1.0); assert_eq!(solution.y[0][2], 20.0); }
621
622 #[test]
623 fn test_bc_segments_boundary_conditions() {
624 let single_segment = vec![BCSegmentData {
626 velocity_min: 1000.0,
627 velocity_max: 2000.0,
628 bc_value: 0.5,
629 }];
630
631 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![
638 BCSegmentData {
639 velocity_min: 0.0,
640 velocity_max: 999.0, bc_value: 0.45,
642 },
643 BCSegmentData {
644 velocity_min: 1000.0,
645 velocity_max: 2000.0,
646 bc_value: 0.50,
647 },
648 ];
649
650 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); }
654
655 #[test]
656 fn test_bc_segments_empty_fallback() {
657 let empty_segments: Vec<BCSegmentData> = vec![];
658
659 let result = get_bc_from_velocity_segments(1500.0, &empty_segments);
661 assert_eq!(result, 0.5); }
663
664 #[test]
665 fn test_fast_integration_params() {
666 let params = FastIntegrationParams {
668 horiz: 1000.0,
669 vert: 0.0,
670 initial_state: [0.0, 0.0, 0.0, 0.0, 50.0, 800.0],
671 t_span: (0.0, 5.0),
672 atmo_params: (0.0, 59.0, 29.92, 0.0),
673 };
674
675 assert_eq!(params.horiz, 1000.0);
676 assert_eq!(params.t_span.0, 0.0);
677 assert_eq!(params.t_span.1, 5.0);
678 assert_eq!(params.initial_state[5], 800.0); }
680
681 #[test]
682 fn test_fast_solution_event_arrays() {
683 let times = vec![0.0, 1.0, 2.0];
684 let states = vec![
685 [0.0, 0.0, 0.0, 800.0, 50.0, 0.0],
686 [800.0, 40.0, 500.0, 750.0, 30.0, 0.0],
687 [1500.0, 20.0, 1000.0, 700.0, 10.0, 0.0],
688 ];
689
690 let t_events = [
692 vec![2.0], vec![0.5], vec![], ];
696
697 let solution = FastSolution::from_trajectory_data(times, states, t_events);
698
699 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()); }
703}