#![allow(dead_code)]
#[derive(Debug, Clone)]
pub struct StabilityParameters {
pub nose_shape_factor: f64,
pub boat_tail_factor: f64,
pub plastic_tip_factor: f64,
pub cop_adjustment: f64,
}
impl StabilityParameters {
pub fn for_bullet_type(bullet_type: &str, has_boat_tail: bool, has_plastic_tip: bool) -> Self {
let mut params = match bullet_type.to_lowercase().as_str() {
"match" | "bthp" => Self {
nose_shape_factor: 0.95,
boat_tail_factor: if has_boat_tail { 0.94 } else { 1.0 },
plastic_tip_factor: 1.0,
cop_adjustment: 0.98,
},
"vld" | "very_low_drag" => Self {
nose_shape_factor: 0.88,
boat_tail_factor: if has_boat_tail { 0.92 } else { 1.0 },
plastic_tip_factor: 1.0,
cop_adjustment: 0.96,
},
"hybrid" => Self {
nose_shape_factor: 0.91,
boat_tail_factor: if has_boat_tail { 0.93 } else { 1.0 },
plastic_tip_factor: 1.0,
cop_adjustment: 0.97,
},
"hunting" => Self {
nose_shape_factor: 0.98,
boat_tail_factor: if has_boat_tail { 0.95 } else { 1.0 },
plastic_tip_factor: if has_plastic_tip { 0.92 } else { 1.0 },
cop_adjustment: 0.99,
},
_ => Self::default(),
};
if has_plastic_tip && params.plastic_tip_factor == 1.0 {
params.plastic_tip_factor = 0.95;
}
params
}
pub fn default() -> Self {
Self {
nose_shape_factor: 1.0,
boat_tail_factor: 1.0,
plastic_tip_factor: 1.0,
cop_adjustment: 1.0,
}
}
}
pub fn calculate_advanced_stability(
mass_grains: f64,
velocity_fps: f64,
twist_rate_inches: f64,
caliber_inches: f64,
length_inches: f64,
air_density_kg_m3: f64,
temperature_k: f64,
bullet_type: &str,
has_boat_tail: bool,
has_plastic_tip: bool,
) -> f64 {
if twist_rate_inches == 0.0 || caliber_inches == 0.0 || length_inches == 0.0 {
return 0.0;
}
let params = StabilityParameters::for_bullet_type(bullet_type, has_boat_tail, has_plastic_tip);
let sg_base = calculate_miller_refined(
mass_grains,
twist_rate_inches,
caliber_inches,
length_inches,
params.nose_shape_factor,
);
let sg_velocity_corrected = apply_velocity_correction(sg_base, velocity_fps);
let sg_atmosphere_corrected =
apply_atmospheric_correction(sg_velocity_corrected, air_density_kg_m3, temperature_k);
let sg_boat_tail = sg_atmosphere_corrected * params.boat_tail_factor;
let sg_plastic_tip = sg_boat_tail * params.plastic_tip_factor;
let sg_final = sg_plastic_tip * params.cop_adjustment;
if velocity_fps > 3000.0 {
apply_bowman_howell_correction(sg_final, velocity_fps, caliber_inches)
} else {
sg_final
}
}
fn calculate_miller_refined(
mass_grains: f64,
twist_rate_inches: f64,
caliber_inches: f64,
length_inches: f64,
nose_shape_factor: f64,
) -> f64 {
let twist_calibers = twist_rate_inches / caliber_inches;
let length_calibers = length_inches / caliber_inches;
const MILLER_CONSTANT: f64 = 30.0;
let inertia_factor = 1.0 + length_calibers.powi(2);
let numerator = MILLER_CONSTANT * mass_grains * nose_shape_factor;
let denominator =
twist_calibers.powi(2) * caliber_inches.powi(3) * length_calibers * inertia_factor;
if denominator == 0.0 {
return 0.0;
}
numerator / denominator
}
fn apply_velocity_correction(sg_base: f64, velocity_fps: f64) -> f64 {
const VELOCITY_REFERENCE: f64 = 2800.0;
if velocity_fps < 1400.0 {
let velocity_factor = (velocity_fps / 1400.0).powf(0.5);
sg_base * velocity_factor * 0.5 } else {
let velocity_factor = (velocity_fps / VELOCITY_REFERENCE).powf(1.0 / 3.0);
sg_base * velocity_factor
}
}
fn apply_atmospheric_correction(sg: f64, air_density_kg_m3: f64, temperature_k: f64) -> f64 {
const STD_DENSITY: f64 = 1.225; const STD_TEMP: f64 = 288.15;
let density_ratio = STD_DENSITY / air_density_kg_m3;
let density_correction = density_ratio.sqrt();
let temp_ratio = temperature_k / STD_TEMP;
let temp_correction = temp_ratio.powf(0.17);
sg * density_correction * temp_correction
}
fn apply_bowman_howell_correction(sg: f64, velocity_fps: f64, caliber_inches: f64) -> f64 {
if velocity_fps <= 3000.0 {
return sg;
}
let excess_velocity = (velocity_fps - 3000.0) / 1000.0;
let mach_correction = 1.0 - 0.05 * excess_velocity.min(2.0);
let caliber_factor = if caliber_inches < 0.264 {
0.95
} else if caliber_inches < 0.308 {
0.97
} else {
1.0
};
sg * mach_correction * caliber_factor
}
pub fn calculate_dynamic_stability(
static_stability: f64,
velocity_mps: f64,
spin_rate_rad_s: f64,
yaw_angle_rad: f64,
caliber_m: f64,
_mass_kg: f64,
) -> f64 {
let spin_param = if velocity_mps > 0.0 {
spin_rate_rad_s * caliber_m / (2.0 * velocity_mps)
} else {
0.0
};
let yaw_factor = 1.0 - 0.1 * yaw_angle_rad.abs().min(0.1);
let precession_factor = 1.0 + 0.05 * spin_param.min(0.5);
static_stability * yaw_factor * precession_factor
}
pub fn predict_stability_at_distance(
initial_stability: f64,
initial_velocity_fps: f64,
current_velocity_fps: f64,
spin_decay_factor: f64, ) -> f64 {
if initial_velocity_fps == 0.0 || current_velocity_fps == 0.0 {
return initial_stability;
}
let velocity_ratio = current_velocity_fps / initial_velocity_fps;
let spin_ratio = velocity_ratio * spin_decay_factor;
let stability_ratio = spin_ratio.powi(2) / velocity_ratio;
initial_stability * stability_ratio
}
pub fn check_trajectory_stability(
muzzle_stability: f64,
muzzle_velocity_fps: f64,
terminal_velocity_fps: f64,
spin_decay_factor: f64,
) -> (bool, f64, String) {
let terminal_stability = predict_stability_at_distance(
muzzle_stability,
muzzle_velocity_fps,
terminal_velocity_fps,
spin_decay_factor,
);
let is_stable = terminal_stability >= 1.3;
let status = if terminal_stability < 1.0 {
"UNSTABLE - Bullet will tumble".to_string()
} else if terminal_stability < 1.3 {
"MARGINAL - May experience accuracy issues".to_string()
} else if terminal_stability < 1.5 {
"ADEQUATE - Acceptable for most conditions".to_string()
} else if terminal_stability < 2.5 {
"GOOD - Optimal stability".to_string()
} else {
"OVER-STABILIZED - May reduce BC slightly".to_string()
};
(is_stable, terminal_stability, status)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_advanced_stability() {
let stability = calculate_advanced_stability(
168.0, 2700.0, 10.0, 0.308, 1.24, 1.225, 288.15, "match", true, false, );
println!("Calculated stability: {}", stability);
assert!(stability > 1.3);
assert!(
stability < 2.5,
"Stability {} exceeds upper bound",
stability
);
}
#[test]
fn test_stability_prediction() {
let (is_stable, terminal_sg, status) = check_trajectory_stability(
2.2, 2700.0, 1900.0, 0.98, );
println!(
"is_stable: {}, terminal_sg: {}, status: {}",
is_stable, terminal_sg, status
);
assert!(
is_stable,
"Expected stable trajectory but got: is_stable={}, terminal_sg={}, status={}",
is_stable, terminal_sg, status
);
assert!(terminal_sg > 1.0, "Terminal SG {} too low", terminal_sg);
assert!(
status.contains("ADEQUATE") || status.contains("GOOD") || status.contains("MARGINAL")
);
}
#[test]
fn test_stability_parameters_bullet_types() {
let match_params = StabilityParameters::for_bullet_type("match", true, false);
let vld_params = StabilityParameters::for_bullet_type("vld", true, false);
let hunting_params = StabilityParameters::for_bullet_type("hunting", true, true);
let default_params = StabilityParameters::for_bullet_type("unknown", false, false);
assert!(vld_params.nose_shape_factor < match_params.nose_shape_factor);
assert!(hunting_params.plastic_tip_factor < 1.0);
assert_eq!(default_params.nose_shape_factor, 1.0);
assert_eq!(default_params.boat_tail_factor, 1.0);
}
#[test]
fn test_stability_edge_cases() {
let zero_twist = calculate_advanced_stability(
168.0, 2700.0, 0.0, 0.308, 1.24, 1.225, 288.15, "match", true, false,
);
assert_eq!(zero_twist, 0.0);
let zero_caliber = calculate_advanced_stability(
168.0, 2700.0, 10.0, 0.0, 1.24, 1.225, 288.15, "match", true, false,
);
assert_eq!(zero_caliber, 0.0);
let zero_length = calculate_advanced_stability(
168.0, 2700.0, 10.0, 0.308, 0.0, 1.225, 288.15, "match", true, false,
);
assert_eq!(zero_length, 0.0);
}
#[test]
fn test_velocity_correction() {
let high_vel = calculate_advanced_stability(
168.0, 3000.0, 10.0, 0.308, 1.24, 1.225, 288.15, "match", true, false,
);
let low_vel = calculate_advanced_stability(
168.0, 2000.0, 10.0, 0.308, 1.24, 1.225, 288.15, "match", true, false,
);
assert!(
high_vel > low_vel,
"Higher velocity ({}) should give higher stability than lower velocity ({})",
high_vel,
low_vel
);
}
#[test]
fn test_hypervelocity_correction() {
let normal_vel = calculate_advanced_stability(
168.0, 2900.0, 10.0, 0.308, 1.24, 1.225, 288.15, "match", true, false,
);
let hyper_vel = calculate_advanced_stability(
168.0, 3500.0, 10.0, 0.308, 1.24, 1.225, 288.15, "match", true, false,
);
assert!(normal_vel > 0.0);
assert!(hyper_vel > 0.0);
}
#[test]
fn test_atmospheric_correction() {
let sea_level = calculate_advanced_stability(
168.0, 2700.0, 10.0, 0.308, 1.24, 1.225, 288.15, "match", true, false,
);
let high_altitude = calculate_advanced_stability(
168.0, 2700.0, 10.0, 0.308, 1.24, 1.0, 288.15, "match", true, false,
);
assert!(
high_altitude > sea_level,
"High altitude ({}) should have higher stability than sea level ({})",
high_altitude,
sea_level
);
}
#[test]
fn test_dynamic_stability() {
let static_sg = 1.5;
let velocity_mps = 800.0;
let spin_rate = 1500.0;
let caliber_m = 0.00782; let mass_kg = 0.0109;
let dynamic_zero_yaw =
calculate_dynamic_stability(static_sg, velocity_mps, spin_rate, 0.0, caliber_m, mass_kg);
let dynamic_with_yaw = calculate_dynamic_stability(
static_sg,
velocity_mps,
spin_rate,
0.05, caliber_m,
mass_kg,
);
assert!(dynamic_with_yaw < dynamic_zero_yaw);
assert!(dynamic_with_yaw > 0.0);
}
#[test]
fn test_predict_stability_at_distance() {
let initial_sg = 1.8;
let initial_vel = 2800.0;
let current_vel = 2000.0;
let spin_decay = 0.97;
let predicted = predict_stability_at_distance(initial_sg, initial_vel, current_vel, spin_decay);
assert!(predicted != initial_sg);
assert!(predicted > 0.0);
}
#[test]
fn test_predict_stability_edge_cases() {
let zero_initial =
predict_stability_at_distance(1.5, 0.0, 2000.0, 0.97);
assert_eq!(zero_initial, 1.5);
let zero_current =
predict_stability_at_distance(1.5, 2800.0, 0.0, 0.97);
assert_eq!(zero_current, 1.5);
}
#[test]
fn test_trajectory_stability_status_messages() {
let (is_stable, sg, status) = check_trajectory_stability(0.8, 2700.0, 1500.0, 0.95);
assert!(!is_stable);
assert!(sg < 1.0);
assert!(status.contains("UNSTABLE"));
let (is_stable, sg, status) = check_trajectory_stability(1.4, 2700.0, 2200.0, 0.98);
assert!(!is_stable || sg >= 1.0);
if sg >= 1.0 && sg < 1.3 {
assert!(status.contains("MARGINAL"));
}
let (_, sg, status) = check_trajectory_stability(4.0, 2700.0, 2500.0, 0.99);
if sg > 2.5 {
assert!(status.contains("OVER-STABILIZED"));
}
}
#[test]
fn test_different_calibers_stability() {
let large_caliber = calculate_advanced_stability(
168.0, 2700.0, 10.0, 0.308, 1.24, 1.225, 288.15, "match", true, false,
);
let small_caliber = calculate_advanced_stability(
90.0, 2700.0, 8.0, 0.264, 1.15, 1.225, 288.15, "match", true, false,
);
assert!(large_caliber > 0.0);
assert!(small_caliber > 0.0);
}
#[test]
fn test_boat_tail_vs_flat_base() {
let boat_tail = calculate_advanced_stability(
168.0, 2700.0, 10.0, 0.308, 1.24, 1.225, 288.15, "match", true, false,
);
let flat_base = calculate_advanced_stability(
168.0, 2700.0, 10.0, 0.308, 1.24, 1.225, 288.15, "match", false, false,
);
assert!(flat_base > boat_tail);
}
}