use crate::{
atmosphere::get_local_atmosphere,
constants::{GRAINS_TO_KG, G_ACCEL_MPS2, MPS_TO_FPS},
drag::get_drag_coefficient,
wind::WindSock,
BCSegmentData, DragModel, InternalBallisticInputs as BallisticInputs,
};
use nalgebra::Vector3;
#[derive(Debug, Clone)]
pub struct FastSolution {
pub t: Vec<f64>,
pub y: Vec<Vec<f64>>,
pub t_events: [Vec<f64>; 3],
pub success: bool,
}
impl FastSolution {
pub fn sol(&self, t_query: &[f64]) -> Vec<Vec<f64>> {
let mut result = vec![vec![0.0; t_query.len()]; 6];
for (i, &tq) in t_query.iter().enumerate() {
let idx = match self
.t
.binary_search_by(|&t| t.partial_cmp(&tq).unwrap_or(std::cmp::Ordering::Greater))
{
Ok(idx) => idx,
Err(idx) => idx,
};
if idx == 0 {
for j in 0..6 {
result[j][i] = self.y[j][0];
}
} else if idx >= self.t.len() {
for j in 0..6 {
result[j][i] = self.y[j][self.t.len() - 1];
}
} else {
let t0 = self.t[idx - 1];
let t1 = self.t[idx];
let frac = (tq - t0) / (t1 - t0);
for j in 0..6 {
let y0 = self.y[j][idx - 1];
let y1 = self.y[j][idx];
result[j][i] = y0 + frac * (y1 - y0);
}
}
}
result
}
pub fn from_trajectory_data(
times: Vec<f64>,
states: Vec<[f64; 6]>,
t_events: [Vec<f64>; 3],
) -> Self {
let n_points = times.len();
let mut y = vec![vec![0.0; n_points]; 6];
for (i, state) in states.iter().enumerate() {
for j in 0..6 {
y[j][i] = state[j];
}
}
FastSolution {
t: times,
y,
t_events,
success: true,
}
}
}
pub struct FastIntegrationParams {
pub horiz: f64,
pub vert: f64,
pub initial_state: [f64; 6],
pub t_span: (f64, f64),
pub atmo_params: (f64, f64, f64, f64),
}
pub fn fast_integrate(
inputs: &BallisticInputs,
wind_sock: &WindSock,
params: FastIntegrationParams,
) -> FastSolution {
let _mass_kg = inputs.bullet_mass * GRAINS_TO_KG;
let bc = inputs.bc_value;
let drag_model = &inputs.bc_type;
let has_bc_segments =
inputs.bc_segments.is_some() && !inputs.bc_segments.as_ref().unwrap().is_empty();
let has_bc_segments_data =
inputs.bc_segments_data.is_some() && !inputs.bc_segments_data.as_ref().unwrap().is_empty();
let dt = if params.horiz > 200.0 {
0.001
} else if params.horiz > 100.0 {
0.0005
} else {
0.0001
};
let v0 = Vector3::new(
params.initial_state[3],
params.initial_state[4],
params.initial_state[5],
)
.norm();
let t_max = if v0 > 1e-6 && params.horiz > 0.0 {
(2.0 * params.horiz / v0).min(params.t_span.1)
} else {
params.t_span.1
};
let n_steps = ((t_max / dt) as usize) + 1;
let mut times = Vec::with_capacity(n_steps);
let mut states = Vec::with_capacity(n_steps);
times.push(0.0);
states.push(params.initial_state);
let (base_density, _) = get_local_atmosphere(
0.0,
params.atmo_params.0,
params.atmo_params.1,
params.atmo_params.2,
params.atmo_params.3,
);
let mut hit_target = false;
let mut hit_ground = false;
let mut max_ord_time = None;
let mut max_ord_y = 0.0;
let ground_threshold = inputs.ground_threshold;
for i in 0..n_steps - 1 {
let t = i as f64 * dt;
let state = states[i];
let pos = Vector3::new(state[0], state[1], state[2]);
let _vel = Vector3::new(state[3], state[4], state[5]);
if pos.z >= params.horiz {
hit_target = true;
times.push(t);
states.push(state);
break;
}
if pos.y <= ground_threshold {
hit_ground = true;
times.push(t);
states.push(state);
break;
}
if pos.y > max_ord_y {
max_ord_y = pos.y;
max_ord_time = Some(t);
}
let k1 = compute_derivatives(
&state,
inputs,
wind_sock,
base_density,
drag_model,
bc,
has_bc_segments,
has_bc_segments_data,
);
let mut state2 = state;
for j in 0..6 {
state2[j] = state[j] + 0.5 * dt * k1[j];
}
let k2 = compute_derivatives(
&state2,
inputs,
wind_sock,
base_density,
drag_model,
bc,
has_bc_segments,
has_bc_segments_data,
);
let mut state3 = state;
for j in 0..6 {
state3[j] = state[j] + 0.5 * dt * k2[j];
}
let k3 = compute_derivatives(
&state3,
inputs,
wind_sock,
base_density,
drag_model,
bc,
has_bc_segments,
has_bc_segments_data,
);
let mut state4 = state;
for j in 0..6 {
state4[j] = state[j] + dt * k3[j];
}
let k4 = compute_derivatives(
&state4,
inputs,
wind_sock,
base_density,
drag_model,
bc,
has_bc_segments,
has_bc_segments_data,
);
let mut new_state = state;
for j in 0..6 {
new_state[j] = state[j] + dt * (k1[j] + 2.0 * k2[j] + 2.0 * k3[j] + k4[j]) / 6.0;
}
times.push(t + dt);
states.push(new_state);
}
let t_events = [
if hit_target {
vec![*times.last().unwrap()]
} else {
vec![]
},
if let Some(t) = max_ord_time {
vec![t]
} else {
vec![]
},
if hit_ground {
vec![*times.last().unwrap()]
} else {
vec![]
},
];
FastSolution::from_trajectory_data(times, states, t_events)
}
fn compute_derivatives(
state: &[f64; 6],
inputs: &BallisticInputs,
wind_sock: &WindSock,
base_density: f64,
drag_model: &DragModel,
bc: f64,
has_bc_segments: bool,
has_bc_segments_data: bool,
) -> [f64; 6] {
let pos = Vector3::new(state[0], state[1], state[2]);
let vel = Vector3::new(state[3], state[4], state[5]);
let wind_vector = wind_sock.vector_for_range_stateless(pos.z);
let vel_adjusted = vel - wind_vector;
let v_mag = vel_adjusted.norm();
let accel = if v_mag < 1e-6 {
Vector3::new(0.0, -G_ACCEL_MPS2, 0.0)
} else {
let v_fps = v_mag * MPS_TO_FPS;
let altitude = inputs.altitude + pos.y;
let (_, speed_of_sound) = get_local_atmosphere(
altitude,
inputs.altitude, inputs.temperature,
inputs.pressure,
if inputs.humidity > 0.0 { inputs.humidity } else { 1.0 },
);
let mach = v_mag / speed_of_sound;
let bc_current = if has_bc_segments_data && inputs.bc_segments_data.is_some() {
get_bc_from_velocity_segments(v_fps, inputs.bc_segments_data.as_ref().unwrap())
} else if has_bc_segments && inputs.bc_segments.is_some() {
crate::derivatives::interpolated_bc(
mach,
inputs.bc_segments.as_ref().unwrap(),
Some(inputs),
)
} else {
bc
};
let drag_factor = get_drag_coefficient(mach, drag_model);
let cd_to_retard = 0.000683 * 0.30;
let standard_factor = drag_factor * cd_to_retard;
let density_scale = base_density / 1.225;
let a_drag_ft_s2 = (v_fps * v_fps) * standard_factor * density_scale / bc_current;
let a_drag_m_s2 = a_drag_ft_s2 * 0.3048; let accel_drag = -a_drag_m_s2 * (vel_adjusted / v_mag);
accel_drag + Vector3::new(0.0, -G_ACCEL_MPS2, 0.0)
};
[vel.x, vel.y, vel.z, accel.x, accel.y, accel.z]
}
fn get_bc_from_velocity_segments(velocity_fps: f64, segments: &[BCSegmentData]) -> f64 {
for segment in segments {
if velocity_fps >= segment.velocity_min && velocity_fps <= segment.velocity_max {
return segment.bc_value;
}
}
if let Some(first) = segments.first() {
if velocity_fps < first.velocity_min {
return first.bc_value;
}
}
if let Some(last) = segments.last() {
if velocity_fps > last.velocity_max {
return last.bc_value;
}
}
0.5
}
pub fn fast_integrate_with_segments(
inputs: &BallisticInputs,
wind_segments: Vec<crate::wind::WindSegment>,
params: FastIntegrationParams,
) -> FastSolution {
use crate::trajectory_integration::{integrate_trajectory, TrajectoryParams};
let mass_kg = inputs.bullet_mass * GRAINS_TO_KG;
let bc = inputs.bc_value;
let drag_model = inputs.bc_type;
let omega_vector = if inputs.enable_advanced_effects {
let omega_earth = 7.2921159e-5; let lat_rad = inputs.latitude.unwrap_or(0.0).to_radians();
let azimuth = inputs.azimuth_angle; Some(Vector3::new(
omega_earth * lat_rad.cos() * azimuth.sin(),
omega_earth * lat_rad.sin(),
omega_earth * lat_rad.cos() * azimuth.cos(),
))
} else {
None
};
let traj_params = TrajectoryParams {
mass_kg,
bc,
drag_model,
wind_segments,
atmos_params: params.atmo_params,
omega_vector,
enable_spin_drift: inputs.enable_advanced_effects,
enable_magnus: inputs.enable_advanced_effects,
enable_coriolis: inputs.enable_advanced_effects,
target_distance_m: params.horiz,
enable_wind_shear: inputs.enable_wind_shear,
wind_shear_model: inputs.wind_shear_model.clone(),
shooter_altitude_m: inputs.altitude,
is_twist_right: inputs.is_twist_right,
custom_drag_table: inputs.custom_drag_table.clone(),
bc_segments: inputs.bc_segments.clone(),
use_bc_segments: inputs.use_bc_segments,
};
let trajectory = integrate_trajectory(
params.initial_state,
params.t_span,
traj_params,
"RK45", 1e-6, 0.01, );
let n_points = trajectory.len();
let mut times = Vec::with_capacity(n_points);
let mut states = Vec::with_capacity(n_points);
let mut target_hit_time: Option<f64> = None;
let mut ground_hit_time: Option<f64> = None;
let mut max_ord_time = None;
let mut max_ord_y = 0.0;
for (t, state_vec) in trajectory {
let state = [
state_vec[0],
state_vec[1],
state_vec[2],
state_vec[3],
state_vec[4],
state_vec[5],
];
if target_hit_time.is_none() && state[2] >= params.horiz {
target_hit_time = Some(t);
}
if ground_hit_time.is_none() && state[1] <= inputs.ground_threshold {
ground_hit_time = Some(t);
}
if state[1] > max_ord_y {
max_ord_y = state[1];
max_ord_time = Some(t);
}
times.push(t);
states.push(state);
}
let t_events = [
if let Some(t) = target_hit_time {
vec![t]
} else {
vec![]
},
if let Some(t) = max_ord_time {
vec![t]
} else {
vec![]
},
if let Some(t) = ground_hit_time {
vec![t]
} else {
vec![]
},
];
FastSolution::from_trajectory_data(times, states, t_events)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_fast_solution_interpolation() {
let times = vec![0.0, 1.0, 2.0];
let states = vec![
[0.0, 0.0, 0.0, 100.0, 50.0, 0.0],
[100.0, 45.0, 0.0, 99.0, 40.0, 0.0],
[198.0, 80.0, 0.0, 98.0, 30.0, 0.0],
];
let solution = FastSolution::from_trajectory_data(times, states, [vec![], vec![], vec![]]);
let result = solution.sol(&[1.5]);
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); }
#[test]
fn test_bc_from_velocity_segments() {
let segments = vec![
BCSegmentData {
velocity_min: 0.0,
velocity_max: 1000.0,
bc_value: 0.5,
},
BCSegmentData {
velocity_min: 1000.0,
velocity_max: 2000.0,
bc_value: 0.52,
},
BCSegmentData {
velocity_min: 2000.0,
velocity_max: 3000.0,
bc_value: 0.55,
},
];
assert_eq!(get_bc_from_velocity_segments(500.0, &segments), 0.5);
assert_eq!(get_bc_from_velocity_segments(1500.0, &segments), 0.52);
assert_eq!(get_bc_from_velocity_segments(2500.0, &segments), 0.55);
assert_eq!(get_bc_from_velocity_segments(-100.0, &segments), 0.5); assert_eq!(get_bc_from_velocity_segments(3500.0, &segments), 0.55); }
#[test]
fn test_fast_solution_interpolation_edge_cases() {
let times = vec![0.0, 1.0, 2.0, 3.0];
let states = vec![
[0.0, 0.0, 0.0, 800.0, 50.0, 0.0],
[800.0, 40.0, 100.0, 750.0, 30.0, 0.0],
[1550.0, 60.0, 200.0, 700.0, 10.0, 0.0],
[2250.0, 50.0, 300.0, 650.0, -10.0, 0.0],
];
let solution = FastSolution::from_trajectory_data(times, states, [vec![], vec![], vec![]]);
let result_before = solution.sol(&[-0.5]);
assert!((result_before[0][0] - 0.0).abs() < 1e-10);
let result_after = solution.sol(&[5.0]);
assert!((result_after[0][0] - 2250.0).abs() < 1e-10);
let result_exact = solution.sol(&[1.0]);
assert!((result_exact[0][0] - 800.0).abs() < 1e-10);
let result_multi = solution.sol(&[0.5, 1.5, 2.5]);
assert_eq!(result_multi[0].len(), 3);
}
#[test]
fn test_fast_solution_from_trajectory_data() {
let times = vec![0.0, 0.5, 1.0];
let states = vec![
[0.0, 1.0, 2.0, 3.0, 4.0, 5.0],
[10.0, 11.0, 12.0, 13.0, 14.0, 15.0],
[20.0, 21.0, 22.0, 23.0, 24.0, 25.0],
];
let t_events = [vec![1.0], vec![0.5], vec![]];
let solution = FastSolution::from_trajectory_data(times.clone(), states, t_events);
assert_eq!(solution.t, times);
assert_eq!(solution.y.len(), 6); assert_eq!(solution.y[0].len(), 3); assert!(solution.success);
assert_eq!(solution.y[0][0], 0.0); assert_eq!(solution.y[1][0], 1.0); assert_eq!(solution.y[0][2], 20.0); }
#[test]
fn test_bc_segments_boundary_conditions() {
let single_segment = vec![BCSegmentData {
velocity_min: 1000.0,
velocity_max: 2000.0,
bc_value: 0.5,
}];
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![
BCSegmentData {
velocity_min: 0.0,
velocity_max: 999.0, bc_value: 0.45,
},
BCSegmentData {
velocity_min: 1000.0,
velocity_max: 2000.0,
bc_value: 0.50,
},
];
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); }
#[test]
fn test_bc_segments_empty_fallback() {
let empty_segments: Vec<BCSegmentData> = vec![];
let result = get_bc_from_velocity_segments(1500.0, &empty_segments);
assert_eq!(result, 0.5); }
#[test]
fn test_fast_integration_params() {
let params = FastIntegrationParams {
horiz: 1000.0,
vert: 0.0,
initial_state: [0.0, 0.0, 0.0, 0.0, 50.0, 800.0],
t_span: (0.0, 5.0),
atmo_params: (0.0, 59.0, 29.92, 0.0),
};
assert_eq!(params.horiz, 1000.0);
assert_eq!(params.t_span.0, 0.0);
assert_eq!(params.t_span.1, 5.0);
assert_eq!(params.initial_state[5], 800.0); }
#[test]
fn test_fast_solution_event_arrays() {
let times = vec![0.0, 1.0, 2.0];
let states = vec![
[0.0, 0.0, 0.0, 800.0, 50.0, 0.0],
[800.0, 40.0, 500.0, 750.0, 30.0, 0.0],
[1500.0, 20.0, 1000.0, 700.0, 10.0, 0.0],
];
let t_events = [
vec![2.0], vec![0.5], vec![], ];
let solution = FastSolution::from_trajectory_data(times, states, t_events);
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()); }
}