#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ProjectileShape {
Spitzer, RoundNose, FlatBase, BoatTail, }
impl ProjectileShape {
pub fn from_str(s: &str) -> Self {
match s.to_lowercase().as_str() {
"spitzer" => Self::Spitzer,
"round_nose" => Self::RoundNose,
"flat_base" => Self::FlatBase,
"boat_tail" => Self::BoatTail,
_ => Self::Spitzer, }
}
}
#[allow(dead_code)]
fn prandtl_glauert_correction(mach: f64) -> f64 {
const MAX_CORRECTION: f64 = 10.0;
const MIN_BETA_SQUARED: f64 = 1.0 / (MAX_CORRECTION * MAX_CORRECTION);
let beta_squared = 1.0 - mach * mach;
if beta_squared <= MIN_BETA_SQUARED {
return MAX_CORRECTION;
}
let beta = beta_squared.sqrt();
1.0 / beta
}
fn critical_mach_number(shape: ProjectileShape) -> f64 {
match shape {
ProjectileShape::Spitzer => 0.85,
ProjectileShape::RoundNose => 0.75,
ProjectileShape::FlatBase => 0.70,
ProjectileShape::BoatTail => 0.88,
}
}
fn sonic_drag_rise(shape: ProjectileShape) -> f64 {
let shape_factor = match shape {
ProjectileShape::RoundNose => 1.2,
_ => 1.0,
};
1.8 * shape_factor
}
fn peak_drag_mach(shape: ProjectileShape) -> f64 {
match shape {
ProjectileShape::Spitzer => 1.05,
_ => 1.02,
}
}
pub fn transonic_drag_rise(mach: f64, shape: ProjectileShape) -> f64 {
let m_crit = critical_mach_number(shape);
let sonic_rise = sonic_drag_rise(shape);
if mach < m_crit {
return 1.0;
}
if mach < 1.0 {
let denominator = 1.0 - m_crit;
if denominator.abs() < f64::EPSILON {
return 1.0; }
let progress = (mach - m_crit) / denominator;
if progress < 0.0 {
return 1.0;
}
let rise_factor = match shape {
ProjectileShape::BoatTail => {
1.0 + 1.2 * progress.powi(2)
}
ProjectileShape::RoundNose => {
1.0 + 2.0 * progress.powf(1.5)
}
_ => {
1.0 + 1.5 * progress.powf(1.8)
}
};
let rise_factor = if mach > 0.92 {
let comp_progress = (mach - 0.92) / 0.08;
let compressibility = 1.0 + 0.5 * comp_progress.powi(3);
rise_factor * compressibility
} else {
rise_factor
};
rise_factor.min(sonic_rise)
} else if mach < 1.2 {
let peak_mach = peak_drag_mach(shape);
if mach <= peak_mach {
sonic_rise * (1.0 + 0.3 * (mach - 1.0))
} else {
let peak_drag = sonic_rise * (1.0 + 0.3 * (peak_mach - 1.0));
let decline_rate = 3.0; peak_drag * (-(decline_rate * (mach - peak_mach))).exp()
}
} else {
1.0
}
}
fn wave_drag_coefficient(mach: f64, shape: ProjectileShape) -> f64 {
const MAX_SUBSONIC_WAVE: f64 = 0.1;
if mach < 0.8 {
return 0.0;
}
if mach < 1.0 {
let m_crit = critical_mach_number(shape);
if mach < m_crit {
return 0.0;
}
let denominator = 1.0 - m_crit;
if denominator.abs() < f64::EPSILON {
return 0.0; }
let progress = (mach - m_crit) / denominator;
MAX_SUBSONIC_WAVE * progress.powi(2)
} else {
let fineness_ratio = 3.5;
let cd_wave_base = 0.15 / fineness_ratio;
let shape_factor = match shape {
ProjectileShape::Spitzer => 0.8, ProjectileShape::RoundNose => 1.2, ProjectileShape::FlatBase => 1.5, ProjectileShape::BoatTail => 0.7, };
let max_mach_factor = MAX_SUBSONIC_WAVE / (cd_wave_base * shape_factor);
let mach_factor = if mach == 1.0 {
max_mach_factor
} else {
(1.0 / (mach * mach - 1.0).sqrt()).min(max_mach_factor)
};
cd_wave_base * mach_factor * shape_factor
}
}
pub fn transonic_correction(
mach: f64,
base_cd: f64,
shape: ProjectileShape,
include_wave_drag: bool,
) -> f64 {
let mut corrected_cd = if include_wave_drag {
base_cd * transonic_drag_rise(mach, shape)
} else {
base_cd
};
if include_wave_drag && mach > 0.8 {
let wave_cd = wave_drag_coefficient(mach, shape);
corrected_cd += wave_cd;
}
corrected_cd
}
pub fn resolve_projectile_shape(
bullet_model: Option<&str>,
caliber: f64,
weight_grains: f64,
g_model: &str,
) -> ProjectileShape {
if let Some(model) = bullet_model {
let m = model.to_lowercase();
if m.contains("boat") || m.contains("bt") {
return ProjectileShape::BoatTail;
} else if m.contains("round") || m.contains("rn") {
return ProjectileShape::RoundNose;
} else if m.contains("flat") || m.contains("fb") {
return ProjectileShape::FlatBase;
}
}
get_projectile_shape(caliber, weight_grains, g_model)
}
pub fn get_projectile_shape(caliber: f64, weight_grains: f64, g_model: &str) -> ProjectileShape {
if g_model == "G7" {
return ProjectileShape::BoatTail;
}
let weight_per_caliber = weight_grains / caliber;
if weight_per_caliber > 500.0 {
return ProjectileShape::BoatTail;
}
if caliber < 0.35 {
ProjectileShape::Spitzer
} else {
ProjectileShape::RoundNose
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mba949_resolve_projectile_shape() {
let c = 0.308;
let w = 168.0;
assert!(matches!(
resolve_projectile_shape(Some("Sierra MatchKing Boat Tail"), c, w, "G1"),
ProjectileShape::BoatTail
));
assert!(matches!(
resolve_projectile_shape(Some("300gr RN"), c, w, "G1"),
ProjectileShape::RoundNose
));
assert!(matches!(
resolve_projectile_shape(Some("flat base"), c, w, "G1"),
ProjectileShape::FlatBase
));
let heuristic = get_projectile_shape(c, w, "G7");
assert_eq!(resolve_projectile_shape(None, c, w, "G7"), heuristic);
assert_eq!(
resolve_projectile_shape(Some("unknown"), c, w, "G7"),
heuristic
);
}
#[test]
fn test_prandtl_glauert() {
assert!((prandtl_glauert_correction(0.5) - 1.1547).abs() < 0.001);
assert!((prandtl_glauert_correction(0.8) - 1.6667).abs() < 0.001);
assert!((prandtl_glauert_correction(0.95) - 3.2026).abs() < 0.001);
}
#[test]
fn prandtl_glauert_follows_the_curve_until_the_cap() {
for mach in [0.99_f64, 0.994] {
let expected = 1.0 / (1.0 - mach * mach).sqrt();
let actual = prandtl_glauert_correction(mach);
assert!((actual - expected).abs() < 1e-12);
assert!(actual < 10.0);
}
assert_eq!(prandtl_glauert_correction(0.995), 10.0);
assert!(prandtl_glauert_correction(f64::NAN).is_nan());
}
#[test]
fn test_critical_mach() {
assert_eq!(critical_mach_number(ProjectileShape::Spitzer), 0.85);
assert_eq!(critical_mach_number(ProjectileShape::BoatTail), 0.88);
assert_eq!(critical_mach_number(ProjectileShape::FlatBase), 0.70);
}
#[test]
fn test_transonic_drag_rise() {
let shape = ProjectileShape::Spitzer;
assert_eq!(transonic_drag_rise(0.8, shape), 1.0);
let rise_0_9 = transonic_drag_rise(0.9, shape);
assert!(rise_0_9 > 1.0 && rise_0_9 < 2.0);
let rise_0_98 = transonic_drag_rise(0.98, shape);
let rise_1_0 = transonic_drag_rise(1.0, shape);
assert!(rise_0_98 > 1.0 && rise_0_98 <= rise_1_0);
let rise_1_1 = transonic_drag_rise(1.1, shape);
assert!(rise_1_1 > 1.5 && rise_1_1 < 2.5);
}
#[test]
fn transonic_drag_rise_is_continuous_at_sonic_and_peak() {
const EPSILON: f64 = 1e-9;
for (shape, peak_mach, expected_sonic, expected_peak) in [
(ProjectileShape::Spitzer, 1.05, 1.8, 1.827),
(ProjectileShape::RoundNose, 1.02, 2.16, 2.17296),
(ProjectileShape::FlatBase, 1.02, 1.8, 1.8108),
(ProjectileShape::BoatTail, 1.02, 1.8, 1.8108),
] {
let sonic = transonic_drag_rise(1.0, shape);
let just_below_sonic = transonic_drag_rise(1.0 - EPSILON, shape);
assert!((sonic - expected_sonic).abs() < 1e-12);
assert!(
(just_below_sonic - sonic).abs() < 1e-6,
"{shape:?} discontinuity at Mach 1: below={just_below_sonic}, at={sonic}"
);
let peak = transonic_drag_rise(peak_mach, shape);
let just_above_peak = transonic_drag_rise(peak_mach + EPSILON, shape);
assert!((peak - expected_peak).abs() < 1e-12);
assert!(
(peak - just_above_peak).abs() < 1e-6,
"{shape:?} discontinuity at peak Mach {peak_mach}: at={peak}, above={just_above_peak}"
);
assert!(peak > sonic, "{shape:?} peak must occur above Mach 1");
assert!(
transonic_drag_rise(peak_mach + 0.01, shape) < peak,
"{shape:?} drag rise must descend after its peak"
);
}
}
#[test]
fn wave_drag_is_bounded_and_continuous_at_sonic() {
const EPSILON: f64 = 1e-9;
for shape in [
ProjectileShape::Spitzer,
ProjectileShape::RoundNose,
ProjectileShape::FlatBase,
ProjectileShape::BoatTail,
] {
let just_below = wave_drag_coefficient(1.0 - EPSILON, shape);
let sonic = wave_drag_coefficient(1.0, shape);
let just_above = wave_drag_coefficient(1.0 + EPSILON, shape);
assert!((sonic - 0.1).abs() < 1e-12);
assert!(
(just_below - sonic).abs() < 1e-6,
"{shape:?} wave drag discontinuity below Mach 1: below={just_below}, at={sonic}"
);
assert!(
(just_above - sonic).abs() < 1e-6,
"{shape:?} wave drag discontinuity above Mach 1: at={sonic}, above={just_above}"
);
for mach in [1.001, 1.01, 1.05, 1.1, 1.2] {
let wave_drag = wave_drag_coefficient(mach, shape);
assert!(
wave_drag <= 0.1 + 1e-12,
"{shape:?} wave drag must stay bounded near sonic: M={mach}, Cd={wave_drag}"
);
}
}
}
#[test]
fn test_projectile_shape_estimation() {
let shape = get_projectile_shape(0.308, 175.0, "G7");
assert!(matches!(shape, ProjectileShape::BoatTail));
let shape1 = get_projectile_shape(0.308, 200.0, "G1");
assert!(matches!(
shape1,
ProjectileShape::Spitzer | ProjectileShape::BoatTail | ProjectileShape::RoundNose
));
let shape2 = get_projectile_shape(0.224, 55.0, "G1");
assert!(matches!(
shape2,
ProjectileShape::Spitzer | ProjectileShape::BoatTail | ProjectileShape::RoundNose
));
let shape3 = get_projectile_shape(0.50, 300.0, "G1");
assert!(matches!(
shape3,
ProjectileShape::Spitzer | ProjectileShape::BoatTail | ProjectileShape::RoundNose
));
}
}