#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq)]
enum FlowRegime {
Laminar, Transitional, Turbulent, }
fn calculate_reynolds_number(
velocity_mps: f64,
diameter_m: f64,
air_density_kg_m3: f64,
temperature_k: f64,
) -> f64 {
let mu = calculate_air_viscosity(temperature_k);
air_density_kg_m3 * velocity_mps * diameter_m / mu
}
fn calculate_air_viscosity(temperature_k: f64) -> f64 {
const T0: f64 = 273.15; const MU0: f64 = 1.716e-5; const S: f64 = 110.4;
MU0 * (T0 + S) / (temperature_k + S) * (temperature_k / T0).powf(1.5)
}
#[allow(dead_code)]
fn get_flow_regime(reynolds_number: f64) -> FlowRegime {
if reynolds_number < 2000.0 {
FlowRegime::Laminar
} else if reynolds_number < 5e5 {
FlowRegime::Transitional
} else {
FlowRegime::Turbulent
}
}
fn clift_gauvin_sphere_cd(reynolds_number: f64) -> f64 {
24.0 / reynolds_number * (1.0 + 0.15 * reynolds_number.powf(0.687))
+ 0.42 / (1.0 + 42_500.0 / reynolds_number.powf(1.16))
}
fn reynolds_drag_correction(reynolds_number: f64, mach: f64, _base_cd: f64) -> f64 {
const FULL_LOW_RE_CORRECTION: f64 = 1e3;
const STANDARD_TABLE_RE: f64 = 1e4;
if !reynolds_number.is_finite() || reynolds_number <= 0.0 {
return 1.0;
}
if reynolds_number >= STANDARD_TABLE_RE {
return 1.0;
}
if mach >= 1.0 {
return 1.0;
}
let reference_cd = clift_gauvin_sphere_cd(STANDARD_TABLE_RE);
let correction = (clift_gauvin_sphere_cd(reynolds_number) / reference_cd).clamp(1.0, 5.0);
const SONIC_FADE_START: f64 = 0.8;
let reynolds_weight = ((STANDARD_TABLE_RE - reynolds_number)
/ (STANDARD_TABLE_RE - FULL_LOW_RE_CORRECTION))
.clamp(0.0, 1.0);
let sonic_weight = ((1.0 - mach) / (1.0 - SONIC_FADE_START)).clamp(0.0, 1.0);
1.0 + (correction - 1.0) * reynolds_weight * sonic_weight
}
fn calculate_corrected_drag(
velocity_mps: f64,
diameter_m: f64,
air_density_kg_m3: f64,
temperature_k: f64,
mach: f64,
base_cd: f64,
) -> (f64, f64) {
let re = calculate_reynolds_number(velocity_mps, diameter_m, air_density_kg_m3, temperature_k);
let correction = reynolds_drag_correction(re, mach, base_cd);
let corrected_cd = base_cd * correction;
(corrected_cd, re)
}
pub fn apply_reynolds_correction(
base_cd: f64,
velocity_mps: f64,
diameter_inches: f64,
air_density_kg_m3: f64,
temperature_c: f64,
mach: f64,
) -> f64 {
let diameter_m = diameter_inches * 0.0254; let temperature_k = temperature_c + 273.15;
if velocity_mps > 1000.0 || mach > 3.0 {
return base_cd;
}
let (corrected_cd, _) = calculate_corrected_drag(
velocity_mps,
diameter_m,
air_density_kg_m3,
temperature_k,
mach,
base_cd,
);
corrected_cd
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_air_viscosity() {
let mu = calculate_air_viscosity(288.15);
assert!((mu - 1.789e-5).abs() < 1e-7);
}
#[test]
fn test_reynolds_number() {
let re = calculate_reynolds_number(100.0, 0.00782, 1.225, 288.15);
assert!(re > 5e4 && re < 6e4);
}
#[test]
fn test_flow_regime() {
assert_eq!(get_flow_regime(1000.0), FlowRegime::Laminar);
assert_eq!(get_flow_regime(1e5), FlowRegime::Transitional);
assert_eq!(get_flow_regime(1e6), FlowRegime::Turbulent);
}
#[test]
fn test_reynolds_correction() {
let correction = reynolds_drag_correction(1e5, 1.5, 0.5);
assert_eq!(correction, 1.0);
let correction = reynolds_drag_correction(500.0, 0.5, 0.5);
assert!(correction > 1.0);
let correction = reynolds_drag_correction(100.0, 0.1, 0.5);
assert!(correction > 1.0);
assert!(correction <= 5.0); }
#[test]
fn low_re_correction_uses_normalized_sphere_drag_curve() {
let cases = [
(10.0, 5.0),
(100.0, 2.624_751_023_486_579),
(1000.0, 1.118_623_125_825_208),
];
for (reynolds_number, expected) in cases {
let actual = reynolds_drag_correction(reynolds_number, 0.5, 0.5);
assert!(
(actual - expected).abs() < 1e-12,
"sphere-curve correction mismatch at Re={reynolds_number}: actual={actual} expected={expected}"
);
}
let mach_faded = reynolds_drag_correction(100.0, 0.9, 0.5);
assert!((mach_faded - 1.812_375_511_743_289).abs() < 1e-12);
for invalid_re in [0.0, -1.0, f64::NAN, f64::INFINITY] {
assert_eq!(reynolds_drag_correction(invalid_re, 0.5, 0.5), 1.0);
}
}
#[test]
fn reynolds_correction_is_continuous_at_low_re_curve_seam() {
let below = reynolds_drag_correction(1000.0 - 1e-6, 0.5, 0.5);
let above = reynolds_drag_correction(1000.0 + 1e-6, 0.5, 0.5);
assert!(
(below - above).abs() < 1e-8,
"low-Re correction jumped at Re=1000: below={below}, above={above}"
);
}
#[test]
fn ordinary_rifle_reynolds_range_uses_standard_table_unchanged() {
let cases = [
(100.0, 100.0 / 340.0, 53_559.675_375),
(300.0, 300.0 / 340.0, 160_679.026_126),
];
for (velocity_mps, mach, expected_re) in cases {
let re = calculate_reynolds_number(velocity_mps, 0.308 * 0.0254, 1.225, 288.15);
assert!((re - expected_re).abs() < 0.1);
let base_cd = 0.5;
let corrected_cd =
apply_reynolds_correction(base_cd, velocity_mps, 0.308, 1.225, 15.0, mach);
assert_eq!(
corrected_cd.to_bits(),
base_cd.to_bits(),
"ordinary rifle Re changed drag at Re={re}"
);
}
}
#[test]
fn reynolds_correction_is_continuous_at_mach_one() {
let base_cd = 0.5;
let below = apply_reynolds_correction(base_cd, 340.0, 0.308, 1.225, 15.0, 1.0 - 1e-6);
let above = apply_reynolds_correction(base_cd, 340.0, 0.308, 1.225, 15.0, 1.0 + 1e-6);
assert!(
(below - above).abs() <= base_cd * 1e-5,
"sonic Reynolds correction jumped: below={below}, above={above}"
);
}
#[test]
fn low_re_residual_fades_to_identity_at_mach_one() {
let below = reynolds_drag_correction(5e3, 1.0 - 1e-8, 0.5);
let above = reynolds_drag_correction(5e3, 1.0 + 1e-8, 0.5);
assert!(
(below - above).abs() <= 1e-6,
"low-Re correction jumped at Mach 1: below={below}, above={above}"
);
}
#[test]
fn reynolds_correction_is_continuous_at_standard_table_threshold() {
let below = reynolds_drag_correction(1e4 - 1e-6, 0.5, 0.5);
let above = reynolds_drag_correction(1e4 + 1e-6, 0.5, 0.5);
assert!(
(below - above).abs() <= 1e-9,
"Reynolds correction jumped at standard-table threshold: below={below}, above={above}"
);
}
}