use crate::atmosphere::{get_direct_atmosphere, get_local_atmosphere};
use crate::bc_estimation::BCSegmentEstimator;
use crate::constants::*;
use crate::drag::get_drag_coefficient_full;
use crate::form_factor::apply_form_factor_to_drag;
use crate::spin_drift::{apply_enhanced_spin_drift, calculate_enhanced_spin_drift};
use crate::InternalBallisticInputs as BallisticInputs;
use nalgebra::Vector3;
const INCHES_PER_FOOT: f64 = 12.0;
const INCHES_TO_METERS: f64 = 0.0254;
const STANDARD_AIR_DENSITY_METRIC: f64 = 1.225;
const MAGNUS_COEFF_SUBSONIC: f64 = 0.030;
const MAGNUS_COEFF_TRANSONIC_REDUCTION: f64 = 0.0075;
const MAGNUS_COEFF_SUPERSONIC_BASE: f64 = 0.015;
const MAGNUS_COEFF_SUPERSONIC_SCALE: f64 = 0.0044;
const MAGNUS_TRANSONIC_LOWER: f64 = 0.8; const MAGNUS_TRANSONIC_UPPER: f64 = 1.2; const MAGNUS_TRANSONIC_RANGE: f64 = 0.4; const MAGNUS_SUPERSONIC_RANGE: f64 = 1.8;
const MAX_REALISTIC_DENSITY: f64 = 2.0; const MIN_REALISTIC_SPEED_OF_SOUND: f64 = 200.0;
fn calculate_spin_rate(twist_rate: f64, velocity_mps: f64) -> f64 {
if twist_rate <= 0.0 {
return 0.0;
}
let velocity_fps = velocity_mps * MPS_TO_FPS;
let twist_rate_ft = twist_rate / INCHES_PER_FOOT;
let revolutions_per_second = velocity_fps / twist_rate_ft;
revolutions_per_second * 2.0 * std::f64::consts::PI
}
fn calculate_magnus_moment_coefficient(mach: f64) -> f64 {
if mach < MAGNUS_TRANSONIC_LOWER {
MAGNUS_COEFF_SUBSONIC
} else if mach < MAGNUS_TRANSONIC_UPPER {
MAGNUS_COEFF_SUBSONIC
- MAGNUS_COEFF_TRANSONIC_REDUCTION * (mach - MAGNUS_TRANSONIC_LOWER)
/ MAGNUS_TRANSONIC_RANGE
} else {
MAGNUS_COEFF_SUPERSONIC_BASE
+ MAGNUS_COEFF_SUPERSONIC_SCALE
* ((mach - MAGNUS_TRANSONIC_UPPER) / MAGNUS_SUPERSONIC_RANGE).min(1.0)
}
}
pub fn compute_derivatives(
pos: Vector3<f64>,
vel: Vector3<f64>,
inputs: &BallisticInputs,
wind_vector: Vector3<f64>,
atmos_params: (f64, f64, f64, f64),
bc_used: f64,
omega_vector: Option<Vector3<f64>>,
time: f64,
) -> [f64; 6] {
let accel_gravity = Vector3::new(0.0, -G_ACCEL_MPS2, 0.0);
let velocity_adjusted = vel - wind_vector;
let speed_air = velocity_adjusted.norm();
let mut accel_drag = Vector3::zeros();
let mut accel_magnus = Vector3::zeros();
if speed_air > crate::constants::MIN_VELOCITY_THRESHOLD {
let v_rel_fps = speed_air * MPS_TO_FPS;
let altitude_at_pos = inputs.altitude + pos[1];
let (air_density, speed_of_sound) = if atmos_params.0 < MAX_REALISTIC_DENSITY
&& atmos_params.1 > MIN_REALISTIC_SPEED_OF_SOUND
&& atmos_params.2 == 0.0
&& atmos_params.3 == 0.0
{
get_direct_atmosphere(atmos_params.0, atmos_params.1)
} else {
get_local_atmosphere(
altitude_at_pos,
atmos_params.0, atmos_params.1, atmos_params.2, atmos_params.3, )
};
let mach = if speed_of_sound > 1e-9 {
speed_air / speed_of_sound
} else {
0.0 };
let mut drag_factor = get_drag_coefficient_full(
mach,
&inputs.bc_type,
true, true, None, if inputs.caliber_inches > 0.0 {
Some(inputs.caliber_inches)
} else {
Some(inputs.bullet_diameter)
},
if inputs.weight_grains > 0.0 {
Some(inputs.weight_grains)
} else {
Some(inputs.bullet_mass * 15.432358)
},
Some(speed_air),
Some(air_density),
Some(atmos_params.1), );
if inputs.use_form_factor {
drag_factor = apply_form_factor_to_drag(
drag_factor,
inputs.bullet_model.as_deref(),
&inputs.bc_type,
true,
);
}
let mut bc_val = bc_used;
if inputs.use_bc_segments {
if inputs.bc_segments_data.is_some() {
bc_val = get_bc_for_velocity(v_rel_fps, inputs, bc_used);
} else if let Some(ref segments) = inputs.bc_segments {
bc_val = interpolated_bc(mach, segments, Some(inputs));
} else {
bc_val = get_bc_for_velocity(v_rel_fps, inputs, bc_used);
}
} else if let Some(ref segments) = inputs.bc_segments {
bc_val = interpolated_bc(mach, segments, Some(inputs));
}
let yaw_deg = if inputs.tipoff_decay_distance.abs() > 1e-9 {
inputs.tipoff_yaw * (-pos[0] / inputs.tipoff_decay_distance).exp()
} else {
inputs.tipoff_yaw };
let yaw_rad = yaw_deg.to_radians();
let yaw_multiplier = 1.0 + yaw_rad.powi(2);
let density_scale = air_density / STANDARD_AIR_DENSITY;
let drag_factor = if mach > 0.7 && mach < 1.3 {
let shape = crate::transonic_drag::get_projectile_shape(
inputs.bullet_diameter,
inputs.bullet_mass,
&inputs.bc_type.to_string(),
);
let corrected_cd = crate::transonic_drag::transonic_correction(
mach,
drag_factor,
shape,
true, );
corrected_cd / drag_factor
} else {
1.0
} * drag_factor;
let drag_factor = if mach < 1.0 && speed_air < 200.0 {
let temperature_c = atmos_params.1;
crate::reynolds::apply_reynolds_correction(
drag_factor,
speed_air,
inputs.bullet_diameter,
air_density,
temperature_c,
mach,
)
} else {
drag_factor
};
let standard_factor = drag_factor * CD_TO_RETARD;
let a_drag_ft_s2 =
(v_rel_fps.powi(2) * standard_factor * yaw_multiplier * density_scale) / bc_val;
let a_drag_m_s2 = a_drag_ft_s2 * FPS_TO_MPS;
accel_drag = -a_drag_m_s2 * (velocity_adjusted / speed_air);
if inputs.enable_advanced_effects && inputs.bullet_diameter > 0.0 && inputs.twist_rate > 0.0
{
let spin_rate_rad_s = calculate_spin_rate(inputs.twist_rate, speed_air);
let c_la = calculate_magnus_moment_coefficient(mach);
let diameter_m = inputs.bullet_diameter * INCHES_TO_METERS;
let spin_param = if speed_air > 1e-9 {
spin_rate_rad_s * diameter_m / (2.0 * speed_air)
} else {
0.0 };
let c_l = spin_param * c_la;
let area = std::f64::consts::PI * (diameter_m / 2.0).powi(2);
const MAGNUS_CALIBRATION_FACTOR: f64 = 1.8; let magnus_force_magnitude =
MAGNUS_CALIBRATION_FACTOR * 0.5 * air_density * speed_air.powi(2) * area * c_l;
let velocity_unit = velocity_adjusted / speed_air;
let vertical = Vector3::new(0.0, 1.0, 0.0);
let magnus_direction = velocity_unit.cross(&vertical);
let magnus_norm = magnus_direction.norm();
if magnus_norm > 1e-12 && magnus_force_magnitude > 1e-12 {
let magnus_direction = magnus_direction / magnus_norm;
let magnus_direction = if inputs.is_twist_right {
magnus_direction
} else {
-magnus_direction
};
let bullet_mass_kg = inputs.bullet_mass * GRAINS_TO_KG;
accel_magnus = (magnus_force_magnitude / bullet_mass_kg) * magnus_direction;
}
}
}
let mut accel = accel_gravity + accel_drag + accel_magnus;
if let Some(omega) = omega_vector {
let accel_coriolis = -2.0 * omega.cross(&vel);
accel += accel_coriolis;
}
let mut derivatives = [vel[0], vel[1], vel[2], accel[0], accel[1], accel[2]];
if inputs.use_enhanced_spin_drift && inputs.enable_advanced_effects && time > 0.0 {
let velocity_adjusted = vel - wind_vector;
let crosswind_speed = if velocity_adjusted.norm() > crate::constants::MIN_VELOCITY_THRESHOLD
{
let trajectory_unit = velocity_adjusted / velocity_adjusted.norm();
let crosswind = wind_vector - wind_vector.dot(&trajectory_unit) * trajectory_unit;
crosswind.norm()
} else {
0.0
};
let air_density = if speed_air > crate::constants::MIN_VELOCITY_THRESHOLD {
let altitude_at_pos = inputs.altitude + pos[1];
let (density, _) = if atmos_params.0 < MAX_REALISTIC_DENSITY
&& atmos_params.1 > MIN_REALISTIC_SPEED_OF_SOUND
&& atmos_params.2 == 0.0
&& atmos_params.3 == 0.0
{
get_direct_atmosphere(atmos_params.0, atmos_params.1)
} else {
get_local_atmosphere(
altitude_at_pos,
atmos_params.0,
atmos_params.1,
atmos_params.2,
atmos_params.3,
)
};
density
} else {
STANDARD_AIR_DENSITY_METRIC };
let spin_components = calculate_enhanced_spin_drift(
inputs.bullet_mass,
vel.norm(),
inputs.twist_rate,
inputs.bullet_diameter,
inputs.bullet_length,
inputs.is_twist_right,
time,
air_density,
crosswind_speed,
0.0, false, );
apply_enhanced_spin_drift(
&mut derivatives,
&spin_components,
time,
inputs.is_twist_right,
);
}
derivatives
}
fn calculate_bc_fallback(
bullet_mass: Option<f64>, bullet_diameter: Option<f64>, bc_type: Option<&str>, ) -> f64 {
use crate::constants::*;
if let Some(weight) = bullet_mass {
let base_bc = if weight < 50.0 {
BC_FALLBACK_ULTRA_LIGHT
} else if weight < 100.0 {
BC_FALLBACK_LIGHT
} else if weight < 150.0 {
BC_FALLBACK_MEDIUM
} else if weight < 200.0 {
BC_FALLBACK_HEAVY
} else {
BC_FALLBACK_VERY_HEAVY
};
return if let Some(drag_model) = bc_type {
if drag_model == "G7" {
base_bc * 0.85 } else {
base_bc
}
} else {
base_bc
};
}
if let Some(caliber) = bullet_diameter {
let base_bc = if caliber <= 0.224 {
BC_FALLBACK_SMALL_CALIBER
} else if caliber <= 0.243 {
BC_FALLBACK_MEDIUM_CALIBER
} else if caliber <= 0.284 {
BC_FALLBACK_LARGE_CALIBER
} else {
BC_FALLBACK_XLARGE_CALIBER
};
return if let Some(drag_model) = bc_type {
if drag_model == "G7" {
base_bc * 0.85 } else {
base_bc
}
} else {
base_bc
};
}
let base_fallback = BC_FALLBACK_CONSERVATIVE;
if let Some(drag_model) = bc_type {
if drag_model == "G7" {
return base_fallback * 0.85;
}
}
base_fallback
}
pub fn interpolated_bc(
mach: f64,
segments: &[(f64, f64)],
inputs: Option<&BallisticInputs>,
) -> f64 {
if segments.is_empty() {
if let Some(inputs) = inputs {
let bc_type_str = match inputs.bc_type {
crate::DragModel::G1 => "G1",
crate::DragModel::G7 => "G7",
_ => "G1", };
return calculate_bc_fallback(
Some(inputs.bullet_mass),
Some(inputs.bullet_diameter),
Some(bc_type_str),
);
}
return crate::constants::BC_FALLBACK_CONSERVATIVE; }
if segments.len() == 1 {
return segments[0].1;
}
let mut sorted_segments = segments.to_vec();
sorted_segments.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
if mach <= sorted_segments[0].0 {
return sorted_segments[0].1;
}
if mach >= sorted_segments[sorted_segments.len() - 1].0 {
return sorted_segments[sorted_segments.len() - 1].1;
}
let idx = sorted_segments.partition_point(|(m, _)| *m <= mach);
if idx == 0 || idx >= sorted_segments.len() {
return sorted_segments[0].1;
}
let (mach1, bc1) = sorted_segments[idx - 1];
let (mach2, bc2) = sorted_segments[idx];
let denominator = mach2 - mach1;
if denominator.abs() < crate::constants::MIN_DIVISION_THRESHOLD {
return bc1; }
let t = (mach - mach1) / denominator;
bc1 + t * (bc2 - bc1)
}
fn get_bc_for_velocity(velocity_fps: f64, inputs: &BallisticInputs, bc_used: f64) -> f64 {
if !inputs.use_bc_segments {
return bc_used;
}
if let Some(ref bc_segments_data) = inputs.bc_segments_data {
for segment in bc_segments_data {
if velocity_fps >= segment.velocity_min && velocity_fps <= segment.velocity_max {
return segment.bc_value;
}
}
}
if inputs.bullet_diameter > 0.0 && inputs.bullet_mass > 0.0 && bc_used > 0.0 {
let model = if let Some(ref bullet_id) = inputs.bullet_id {
bullet_id.clone()
} else {
format!("{}gr bullet", inputs.bullet_mass as i32)
};
let bc_type_str = inputs.bc_type_str.as_deref().unwrap_or("G1");
let segments = BCSegmentEstimator::estimate_bc_segments(
bc_used,
inputs.caliber_inches,
inputs.weight_grains,
&model,
bc_type_str,
);
for segment in &segments {
if velocity_fps >= segment.velocity_min && velocity_fps <= segment.velocity_max {
return segment.bc_value;
}
}
}
bc_used
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_inputs() -> BallisticInputs {
BallisticInputs {
muzzle_velocity: 800.0,
bc_value: 0.5,
bullet_mass: 0.0109, bullet_diameter: 0.00782, altitude: 1000.0,
..Default::default()
}
}
#[test]
fn test_compute_derivatives_basic() {
let pos = Vector3::new(0.0, 0.0, 0.0);
let vel = Vector3::new(800.0, 0.0, 0.0);
let inputs = create_test_inputs();
let wind_vector = Vector3::zeros();
let atmos_params = (1.225, 340.0, 0.0, 0.0); let bc_used = 0.5;
let result = compute_derivatives(
pos,
vel,
&inputs,
wind_vector,
atmos_params,
bc_used,
None,
0.0,
);
assert_eq!(result.len(), 6);
assert!((result[0] - vel[0]).abs() < 1e-10);
assert!((result[1] - vel[1]).abs() < 1e-10);
assert!((result[2] - vel[2]).abs() < 1e-10);
assert!(result[4] < 0.0);
assert!(result[3] < 0.0); }
#[test]
fn test_compute_derivatives_with_wind() {
let pos = Vector3::new(0.0, 0.0, 0.0);
let vel = Vector3::new(800.0, 0.0, 0.0);
let inputs = create_test_inputs();
let wind_vector = Vector3::new(10.0, 0.0, 0.0); let atmos_params = (1.225, 340.0, 0.0, 0.0); let bc_used = 0.5;
let result = compute_derivatives(
pos,
vel,
&inputs,
wind_vector,
atmos_params,
bc_used,
None,
0.0,
);
assert!(result[3] < 0.0); }
#[test]
fn test_compute_derivatives_with_coriolis() {
let pos = Vector3::new(0.0, 0.0, 0.0);
let vel = Vector3::new(800.0, 0.0, 0.0);
let inputs = create_test_inputs();
let wind_vector = Vector3::zeros();
let atmos_params = (1.225, 340.0, 0.0, 0.0); let bc_used = 0.5;
let omega = Vector3::new(0.0, 0.0, 7.2921e-5);
let result = compute_derivatives(
pos,
vel,
&inputs,
wind_vector,
atmos_params,
bc_used,
Some(omega),
0.0,
);
assert!(result[4].abs() > 1e-3); }
#[test]
fn test_interpolated_bc() {
let segments = vec![(0.5, 0.4), (1.0, 0.5), (1.5, 0.6), (2.0, 0.5)];
assert!((interpolated_bc(1.0, &segments, None) - 0.5).abs() < 1e-10);
let bc_075 = interpolated_bc(0.75, &segments, None);
assert!(bc_075 > 0.4 && bc_075 < 0.5);
assert!((interpolated_bc(0.1, &segments, None) - 0.4).abs() < 1e-10);
assert!((interpolated_bc(3.0, &segments, None) - 0.5).abs() < 1e-10);
}
#[test]
fn test_interpolated_bc_edge_cases() {
assert!(
(interpolated_bc(1.0, &[], None) - crate::constants::BC_FALLBACK_CONSERVATIVE).abs()
< 1e-10
);
let single = vec![(1.0, 0.7)];
assert!((interpolated_bc(1.5, &single, None) - 0.7).abs() < 1e-10);
}
#[test]
fn test_magnus_effect() {
let pos = Vector3::new(0.0, 0.0, 0.0);
let vel = Vector3::new(822.96, 0.0, 0.0); let mut inputs = create_test_inputs();
inputs.bullet_diameter = 0.308; inputs.twist_rate = 10.0; inputs.is_twist_right = true;
inputs.enable_advanced_effects = true;
let wind_vector = Vector3::zeros();
let atmos_params = (1.225, 340.0, 0.0, 0.0); let bc_used = 0.5;
let result = compute_derivatives(
pos,
vel,
&inputs,
wind_vector,
atmos_params,
bc_used,
None,
0.0,
);
assert!(result[5].abs() > 0.01); assert!(result[5] > 0.0);
}
#[test]
fn test_magnus_moment_coefficient() {
assert!((calculate_magnus_moment_coefficient(0.5) - 0.030).abs() < 0.001); assert!((calculate_magnus_moment_coefficient(0.8) - 0.030).abs() < 0.001); assert!((calculate_magnus_moment_coefficient(1.0) - 0.02625).abs() < 0.001); assert!((calculate_magnus_moment_coefficient(1.2) - 0.015).abs() < 0.001); assert!((calculate_magnus_moment_coefficient(2.0) - 0.01653).abs() < 0.001);
}
}