1#[allow(dead_code)]
9#[derive(Debug, Clone, Copy, PartialEq)]
10enum FlowRegime {
11 Laminar, Transitional, Turbulent, }
15
16fn calculate_reynolds_number(
26 velocity_mps: f64,
27 diameter_m: f64,
28 air_density_kg_m3: f64,
29 temperature_k: f64,
30) -> f64 {
31 let mu = calculate_air_viscosity(temperature_k);
32 air_density_kg_m3 * velocity_mps * diameter_m / mu
33}
34
35fn calculate_air_viscosity(temperature_k: f64) -> f64 {
43 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)
50}
51
52#[allow(dead_code)]
54fn get_flow_regime(reynolds_number: f64) -> FlowRegime {
55 if reynolds_number < 2000.0 {
56 FlowRegime::Laminar
57 } else if reynolds_number < 5e5 {
58 FlowRegime::Transitional
59 } else {
60 FlowRegime::Turbulent
61 }
62}
63
64fn clift_gauvin_sphere_cd(reynolds_number: f64) -> f64 {
68 24.0 / reynolds_number * (1.0 + 0.15 * reynolds_number.powf(0.687))
69 + 0.42 / (1.0 + 42_500.0 / reynolds_number.powf(1.16))
70}
71
72fn reynolds_drag_correction(reynolds_number: f64, mach: f64, _base_cd: f64) -> f64 {
86 const FULL_LOW_RE_CORRECTION: f64 = 1e3;
90 const STANDARD_TABLE_RE: f64 = 1e4;
91
92 if !reynolds_number.is_finite() || reynolds_number <= 0.0 {
93 return 1.0;
94 }
95
96 if reynolds_number >= STANDARD_TABLE_RE {
97 return 1.0;
98 }
99
100 if mach >= 1.0 {
102 return 1.0;
103 }
104
105 let reference_cd = clift_gauvin_sphere_cd(STANDARD_TABLE_RE);
109 let correction = (clift_gauvin_sphere_cd(reynolds_number) / reference_cd).clamp(1.0, 5.0);
110
111 const SONIC_FADE_START: f64 = 0.8;
115 let reynolds_weight = ((STANDARD_TABLE_RE - reynolds_number)
116 / (STANDARD_TABLE_RE - FULL_LOW_RE_CORRECTION))
117 .clamp(0.0, 1.0);
118 let sonic_weight = ((1.0 - mach) / (1.0 - SONIC_FADE_START)).clamp(0.0, 1.0);
119
120 1.0 + (correction - 1.0) * reynolds_weight * sonic_weight
121}
122
123fn calculate_corrected_drag(
136 velocity_mps: f64,
137 diameter_m: f64,
138 air_density_kg_m3: f64,
139 temperature_k: f64,
140 mach: f64,
141 base_cd: f64,
142) -> (f64, f64) {
143 let re = calculate_reynolds_number(velocity_mps, diameter_m, air_density_kg_m3, temperature_k);
144 let correction = reynolds_drag_correction(re, mach, base_cd);
145 let corrected_cd = base_cd * correction;
146
147 (corrected_cd, re)
148}
149
150pub fn apply_reynolds_correction(
167 base_cd: f64,
168 velocity_mps: f64,
169 diameter_inches: f64,
170 air_density_kg_m3: f64,
171 temperature_c: f64,
172 mach: f64,
173) -> f64 {
174 let diameter_m = diameter_inches * 0.0254; let temperature_k = temperature_c + 273.15; if velocity_mps > 1000.0 || mach > 3.0 {
180 return base_cd;
181 }
182
183 let (corrected_cd, _) = calculate_corrected_drag(
185 velocity_mps,
186 diameter_m,
187 air_density_kg_m3,
188 temperature_k,
189 mach,
190 base_cd,
191 );
192
193 corrected_cd
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199
200 #[test]
201 fn test_air_viscosity() {
202 let mu = calculate_air_viscosity(288.15);
204 assert!((mu - 1.789e-5).abs() < 1e-7);
205 }
206
207 #[test]
208 fn test_reynolds_number() {
209 let re = calculate_reynolds_number(100.0, 0.00782, 1.225, 288.15);
210 assert!(re > 5e4 && re < 6e4);
212 }
213
214 #[test]
215 fn test_flow_regime() {
216 assert_eq!(get_flow_regime(1000.0), FlowRegime::Laminar);
217 assert_eq!(get_flow_regime(1e5), FlowRegime::Transitional);
218 assert_eq!(get_flow_regime(1e6), FlowRegime::Turbulent);
219 }
220
221 #[test]
222 fn test_reynolds_correction() {
223 let correction = reynolds_drag_correction(1e5, 1.5, 0.5);
225 assert_eq!(correction, 1.0);
226
227 let correction = reynolds_drag_correction(500.0, 0.5, 0.5);
229 assert!(correction > 1.0);
230
231 let correction = reynolds_drag_correction(100.0, 0.1, 0.5);
233 assert!(correction > 1.0);
235 assert!(correction <= 5.0); }
237
238 #[test]
239 fn low_re_correction_uses_normalized_sphere_drag_curve() {
240 let cases = [
241 (10.0, 5.0),
242 (100.0, 2.624_751_023_486_579),
243 (1000.0, 1.118_623_125_825_208),
244 ];
245
246 for (reynolds_number, expected) in cases {
247 let actual = reynolds_drag_correction(reynolds_number, 0.5, 0.5);
248 assert!(
249 (actual - expected).abs() < 1e-12,
250 "sphere-curve correction mismatch at Re={reynolds_number}: actual={actual} expected={expected}"
251 );
252 }
253
254 let mach_faded = reynolds_drag_correction(100.0, 0.9, 0.5);
255 assert!((mach_faded - 1.812_375_511_743_289).abs() < 1e-12);
256
257 for invalid_re in [0.0, -1.0, f64::NAN, f64::INFINITY] {
258 assert_eq!(reynolds_drag_correction(invalid_re, 0.5, 0.5), 1.0);
259 }
260 }
261
262 #[test]
263 fn reynolds_correction_is_continuous_at_low_re_curve_seam() {
264 let below = reynolds_drag_correction(1000.0 - 1e-6, 0.5, 0.5);
265 let above = reynolds_drag_correction(1000.0 + 1e-6, 0.5, 0.5);
266
267 assert!(
268 (below - above).abs() < 1e-8,
269 "low-Re correction jumped at Re=1000: below={below}, above={above}"
270 );
271 }
272
273 #[test]
274 fn ordinary_rifle_reynolds_range_uses_standard_table_unchanged() {
275 let cases = [
276 (100.0, 100.0 / 340.0, 53_559.675_375),
277 (300.0, 300.0 / 340.0, 160_679.026_126),
278 ];
279
280 for (velocity_mps, mach, expected_re) in cases {
281 let re = calculate_reynolds_number(velocity_mps, 0.308 * 0.0254, 1.225, 288.15);
282 assert!((re - expected_re).abs() < 0.1);
283
284 let base_cd = 0.5;
285 let corrected_cd =
286 apply_reynolds_correction(base_cd, velocity_mps, 0.308, 1.225, 15.0, mach);
287
288 assert_eq!(
289 corrected_cd.to_bits(),
290 base_cd.to_bits(),
291 "ordinary rifle Re changed drag at Re={re}"
292 );
293 }
294 }
295
296 #[test]
297 fn reynolds_correction_is_continuous_at_mach_one() {
298 let base_cd = 0.5;
299 let below = apply_reynolds_correction(base_cd, 340.0, 0.308, 1.225, 15.0, 1.0 - 1e-6);
300 let above = apply_reynolds_correction(base_cd, 340.0, 0.308, 1.225, 15.0, 1.0 + 1e-6);
301
302 assert!(
303 (below - above).abs() <= base_cd * 1e-5,
304 "sonic Reynolds correction jumped: below={below}, above={above}"
305 );
306 }
307
308 #[test]
309 fn low_re_residual_fades_to_identity_at_mach_one() {
310 let below = reynolds_drag_correction(5e3, 1.0 - 1e-8, 0.5);
311 let above = reynolds_drag_correction(5e3, 1.0 + 1e-8, 0.5);
312
313 assert!(
314 (below - above).abs() <= 1e-6,
315 "low-Re correction jumped at Mach 1: below={below}, above={above}"
316 );
317 }
318
319 #[test]
320 fn reynolds_correction_is_continuous_at_standard_table_threshold() {
321 let below = reynolds_drag_correction(1e4 - 1e-6, 0.5, 0.5);
322 let above = reynolds_drag_correction(1e4 + 1e-6, 0.5, 0.5);
323
324 assert!(
325 (below - above).abs() <= 1e-9,
326 "Reynolds correction jumped at standard-table threshold: below={below}, above={above}"
327 );
328 }
329}
330
331