1use std::f64::consts::PI;
10
11#[derive(Debug, Clone, Copy)]
13pub struct PitchDampingCoefficients {
14 pub subsonic: f64, pub transonic_low: f64, pub transonic_high: f64, pub supersonic: f64, }
19
20impl Default for PitchDampingCoefficients {
21 fn default() -> Self {
22 Self {
23 subsonic: -0.8,
24 transonic_low: -0.3,
25 transonic_high: 0.2,
26 supersonic: -0.5,
27 }
28 }
29}
30
31impl PitchDampingCoefficients {
32 pub fn from_bullet_type(bullet_type: &str) -> Self {
34 match bullet_type.to_lowercase().as_str() {
35 "match_boat_tail" => Self {
36 subsonic: -0.9,
37 transonic_low: -0.4,
38 transonic_high: 0.1,
39 supersonic: -0.6,
40 },
41 "match_flat_base" => Self {
42 subsonic: -0.7,
43 transonic_low: -0.2,
44 transonic_high: 0.3,
45 supersonic: -0.4,
46 },
47 "vld" => Self {
48 subsonic: -1.0,
50 transonic_low: -0.5,
51 transonic_high: -0.1,
52 supersonic: -0.7,
53 },
54 "hunting" => Self {
55 subsonic: -0.6,
56 transonic_low: -0.1,
57 transonic_high: 0.4,
58 supersonic: -0.3,
59 },
60 "fmj" => Self {
61 subsonic: -0.7,
62 transonic_low: -0.2,
63 transonic_high: 0.2,
64 supersonic: -0.5,
65 },
66 _ => Self::default(),
67 }
68 }
69}
70
71pub fn calculate_pitch_damping_coefficient(mach: f64, coeffs: &PitchDampingCoefficients) -> f64 {
73 if mach < 0.8 {
74 coeffs.subsonic
76 } else if mach < 1.0 {
77 let t = (mach - 0.8) / 0.2;
80 coeffs.subsonic * (1.0 - t) + coeffs.transonic_low * t
81 } else if mach < 1.2 {
82 let t = (mach - 1.0) / 0.2;
85 coeffs.transonic_low * (1.0 - t) + coeffs.transonic_high * t
86 } else {
87 let t = ((mach - 1.2) / 0.8).min(1.0);
90 coeffs.transonic_high * (1.0 - t) + coeffs.supersonic * t
91 }
92}
93
94pub fn calculate_pitch_damping_moment(
96 pitch_rate_rad_s: f64,
97 velocity_mps: f64,
98 air_density_kg_m3: f64,
99 caliber_m: f64,
100 _length_m: f64,
101 mach: f64,
102 coeffs: &PitchDampingCoefficients,
103) -> f64 {
104 if velocity_mps == 0.0 || pitch_rate_rad_s == 0.0 {
105 return 0.0;
106 }
107
108 let cmq = calculate_pitch_damping_coefficient(mach, coeffs);
110
111 let q = 0.5 * air_density_kg_m3 * velocity_mps.powi(2);
113
114 let s = PI * (caliber_m / 2.0).powi(2);
116
117 let d = caliber_m;
119
120 let q_nondim = pitch_rate_rad_s * d / velocity_mps;
122
123 q * s * d * cmq * q_nondim
126}
127
128pub fn calculate_transverse_moment_of_inertia(
130 mass_kg: f64,
131 caliber_m: f64,
132 length_m: f64,
133 shape: &str,
134) -> f64 {
135 let radius = caliber_m / 2.0;
136
137 match shape {
138 "cylinder" => {
139 mass_kg * (3.0 * radius.powi(2) + length_m.powi(2)) / 12.0
141 }
142 "ogive" => {
143 let cylinder_i = mass_kg * (3.0 * radius.powi(2) + length_m.powi(2)) / 12.0;
146 0.85 * cylinder_i
147 }
148 "boat_tail" => {
149 let cylinder_i = mass_kg * (3.0 * radius.powi(2) + length_m.powi(2)) / 12.0;
152 0.80 * cylinder_i
153 }
154 _ => {
155 mass_kg * (3.0 * radius.powi(2) + length_m.powi(2)) / 12.0
157 }
158 }
159}
160
161pub fn calculate_angular_acceleration(moment: f64, moment_of_inertia: f64) -> f64 {
163 if moment_of_inertia > 0.0 {
164 moment / moment_of_inertia
165 } else {
166 0.0
167 }
168}
169
170pub fn calculate_damped_yaw_of_repose(
172 stability_factor: f64,
173 velocity_mps: f64,
174 spin_rate_rad_s: f64,
175 wind_velocity_mps: f64,
176 pitch_rate_rad_s: f64,
177 air_density_kg_m3: f64,
178 caliber_inches: f64,
179 length_inches: f64,
180 mass_grains: f64,
181 mach: f64,
182 bullet_type: &str,
183) -> (f64, f64) {
184 if stability_factor <= 1.0 || spin_rate_rad_s == 0.0 {
185 return (0.0, 0.0);
186 }
187
188 let caliber_m = caliber_inches * 0.0254;
190 let length_m = length_inches * 0.0254;
191 let mass_kg = mass_grains * 0.00006479891;
192
193 let base_yaw_rad = if wind_velocity_mps != 0.0 && velocity_mps > 0.0 {
195 (wind_velocity_mps / velocity_mps).atan()
196 } else {
197 0.002 };
200
201 let coeffs = PitchDampingCoefficients::from_bullet_type(bullet_type);
203
204 let damping_moment = calculate_pitch_damping_moment(
206 pitch_rate_rad_s,
207 velocity_mps,
208 air_density_kg_m3,
209 caliber_m,
210 length_m,
211 mach,
212 &coeffs,
213 );
214
215 let i_transverse =
217 calculate_transverse_moment_of_inertia(mass_kg, caliber_m, length_m, "ogive");
218
219 let angular_accel = calculate_angular_acceleration(damping_moment, i_transverse);
221
222 let time_constant = if angular_accel != 0.0 && pitch_rate_rad_s != 0.0 {
224 (pitch_rate_rad_s / angular_accel).abs()
225 } else {
226 10.0 };
228
229 let convergence_rate = 1.0 / time_constant;
231
232 let stability_factor_clamped = stability_factor.min(10.0);
234 let mut damping_factor = 1.0 / (1.0 + (stability_factor_clamped - 1.0).sqrt());
235
236 if (0.8..=1.2).contains(&mach) {
238 let cmq = calculate_pitch_damping_coefficient(mach, &coeffs);
239 if cmq > 0.0 {
240 damping_factor *= 1.0 + cmq.abs();
242 }
243 }
244
245 let equilibrium_yaw_rad = base_yaw_rad * damping_factor;
246
247 (equilibrium_yaw_rad, convergence_rate)
248}
249
250pub fn calculate_precession_with_damping(
252 yaw_angle_rad: f64,
253 spin_rate_rad_s: f64,
254 velocity_mps: f64,
255 pitch_damping_moment: f64,
256 transverse_inertia: f64,
257 spin_inertia: f64,
258) -> f64 {
259 if spin_rate_rad_s == 0.0 || velocity_mps == 0.0 {
260 return 0.0;
261 }
262
263 let basic_precession = (spin_inertia * spin_rate_rad_s * yaw_angle_rad.sin())
265 / (transverse_inertia * velocity_mps);
266
267 let damping_factor = if transverse_inertia > 0.0 {
269 let factor = 1.0 - (pitch_damping_moment.abs() / (transverse_inertia * velocity_mps));
270 factor.max(0.1) } else {
272 1.0
273 };
274
275 basic_precession * damping_factor
276}
277
278#[cfg(test)]
279mod tests {
280 use super::*;
281
282 #[test]
283 fn test_pitch_damping_coefficient() {
284 let coeffs = PitchDampingCoefficients::default();
285
286 assert_eq!(calculate_pitch_damping_coefficient(0.5, &coeffs), -0.8);
288
289 let transonic = calculate_pitch_damping_coefficient(0.9, &coeffs);
291 assert!(transonic > -0.8 && transonic < -0.3);
292
293 let supersonic = calculate_pitch_damping_coefficient(2.0, &coeffs);
295 assert_eq!(supersonic, -0.5);
296 }
297
298 #[test]
299 fn test_pitch_damping_moment() {
300 let coeffs = PitchDampingCoefficients::default();
301 let moment = calculate_pitch_damping_moment(
302 0.1, 300.0, 1.225, 0.00782, 0.033, 0.87, &coeffs,
309 );
310
311 assert!(moment < 0.0);
313 }
314
315 #[test]
316 fn test_bullet_type_coefficients() {
317 let types = [
318 "match_boat_tail",
319 "match_flat_base",
320 "vld",
321 "hunting",
322 "fmj",
323 "unknown",
324 ];
325
326 for bullet_type in &types {
327 let coeffs = PitchDampingCoefficients::from_bullet_type(bullet_type);
328
329 assert!(coeffs.subsonic < 0.0);
331
332 assert!(coeffs.supersonic < 0.0);
334
335 if *bullet_type == "vld" {
337 let default_coeffs = PitchDampingCoefficients::default();
338 assert!(coeffs.subsonic < default_coeffs.subsonic);
339 }
340 }
341 }
342
343 #[test]
344 fn test_transonic_instability() {
345 let coeffs = PitchDampingCoefficients::from_bullet_type("hunting");
346
347 assert!(coeffs.transonic_high > 0.0);
349
350 let mach_1_1 = calculate_pitch_damping_coefficient(1.1, &coeffs);
352
353 assert!(mach_1_1 > coeffs.transonic_low);
355 }
356
357 #[test]
358 fn test_transverse_moment_of_inertia() {
359 let mass_kg = 0.01134; let caliber_m = 0.00782; let length_m = 0.033; let i_cylinder =
364 calculate_transverse_moment_of_inertia(mass_kg, caliber_m, length_m, "cylinder");
365 let i_ogive = calculate_transverse_moment_of_inertia(mass_kg, caliber_m, length_m, "ogive");
366 let i_boat_tail =
367 calculate_transverse_moment_of_inertia(mass_kg, caliber_m, length_m, "boat_tail");
368 let i_unknown =
369 calculate_transverse_moment_of_inertia(mass_kg, caliber_m, length_m, "unknown");
370
371 assert!(i_cylinder > i_ogive);
373 assert!(i_ogive > i_boat_tail);
374 assert_eq!(i_cylinder, i_unknown);
375
376 assert!(i_cylinder > 0.0);
378 assert!(i_cylinder < 1.0); }
380
381 #[test]
382 fn test_angular_acceleration() {
383 let moment = -0.001; let inertia = 0.0001; let accel = calculate_angular_acceleration(moment, inertia);
387 assert_eq!(accel, moment / inertia);
388
389 let accel_zero = calculate_angular_acceleration(moment, 0.0);
391 assert_eq!(accel_zero, 0.0);
392 }
393
394 #[test]
395 fn test_damped_yaw_of_repose() {
396 let (yaw, convergence) = calculate_damped_yaw_of_repose(
397 2.5, 800.0, 19000.0, 10.0, 0.01, 1.225, 0.308, 1.3, 175.0, 0.9, "match_boat_tail",
408 );
409
410 assert!(yaw > 0.0);
412 assert!(yaw < 0.1); assert!(convergence > 0.0);
414
415 let (yaw_unstable, conv_unstable) = calculate_damped_yaw_of_repose(
417 0.9,
418 800.0,
419 19000.0,
420 10.0,
421 0.01,
422 1.225,
423 0.308,
424 1.3,
425 175.0,
426 0.9,
427 "match_boat_tail",
428 );
429 assert_eq!(yaw_unstable, 0.0);
430 assert_eq!(conv_unstable, 0.0);
431 }
432
433 #[test]
434 fn test_precession_with_damping() {
435 let precession = calculate_precession_with_damping(
436 0.01, 19000.0, 800.0, -0.001, 0.0001, 0.00005, );
443
444 assert!(precession > 0.0);
445
446 let precession_zero =
448 calculate_precession_with_damping(0.01, 0.0, 800.0, -0.001, 0.0001, 0.00005);
449 assert_eq!(precession_zero, 0.0);
450
451 let precession_no_vel =
453 calculate_precession_with_damping(0.01, 19000.0, 0.0, -0.001, 0.0001, 0.00005);
454 assert_eq!(precession_no_vel, 0.0);
455 }
456
457 #[test]
458 fn test_mach_interpolation() {
459 let coeffs = PitchDampingCoefficients::default();
460
461 let mach_values = [0.7, 0.8, 0.9, 1.0, 1.1, 1.2, 1.5, 2.0];
463 let mut last_value = calculate_pitch_damping_coefficient(mach_values[0], &coeffs);
464
465 for &mach in &mach_values[1..] {
466 let value = calculate_pitch_damping_coefficient(mach, &coeffs);
467
468 assert!((value - last_value).abs() < 1.0);
470 last_value = value;
471 }
472 }
473
474 #[test]
475 fn test_pitch_damping_edge_cases() {
476 let coeffs = PitchDampingCoefficients::default();
477
478 let moment_zero_pitch =
480 calculate_pitch_damping_moment(0.0, 300.0, 1.225, 0.00782, 0.033, 0.87, &coeffs);
481 assert_eq!(moment_zero_pitch, 0.0);
482
483 let moment_zero_vel =
485 calculate_pitch_damping_moment(0.1, 0.0, 1.225, 0.00782, 0.033, 0.87, &coeffs);
486 assert_eq!(moment_zero_vel, 0.0);
487 }
488
489 #[test]
490 fn test_default_implementation() {
491 let coeffs1 = PitchDampingCoefficients::default();
492 let coeffs2 = PitchDampingCoefficients::from_bullet_type("unknown");
493
494 assert_eq!(coeffs1.subsonic, coeffs2.subsonic);
495 assert_eq!(coeffs1.transonic_low, coeffs2.transonic_low);
496 assert_eq!(coeffs1.transonic_high, coeffs2.transonic_high);
497 assert_eq!(coeffs1.supersonic, coeffs2.supersonic);
498 }
499
500 #[test]
501 fn test_transonic_jump() {
502 let _coeffs = PitchDampingCoefficients::from_bullet_type("hunting");
503
504 let (yaw_subsonic, _) = calculate_damped_yaw_of_repose(
506 2.5, 250.0, 19000.0, 10.0, 0.01, 1.225, 0.308, 1.3, 175.0, 0.7, "hunting",
507 );
508
509 let (yaw_transonic, _) = calculate_damped_yaw_of_repose(
510 2.5, 343.0, 19000.0, 10.0, 0.01, 1.225, 0.308, 1.3, 175.0, 1.0, "hunting",
511 );
512
513 assert!(yaw_subsonic > 0.0);
515 assert!(yaw_transonic > 0.0);
516 }
517}