1use crate::constants::G_ACCEL_MPS2;
12use crate::pitch_damping::{calculate_pitch_damping_moment, PitchDampingCoefficients};
13
14pub(crate) fn projectile_moments_of_inertia(
20 mass_kg: f64,
21 caliber_m: f64,
22 length_m: f64,
23) -> (f64, f64) {
24 if !mass_kg.is_finite()
25 || mass_kg <= 0.0
26 || !caliber_m.is_finite()
27 || caliber_m <= 0.0
28 || !length_m.is_finite()
29 || length_m <= 0.0
30 {
31 return (0.0, 0.0);
32 }
33
34 let spin_inertia =
35 crate::spin_decay::calculate_moment_of_inertia(mass_kg, caliber_m, length_m, "ogive");
36 let transverse_inertia = crate::pitch_damping::calculate_transverse_moment_of_inertia(
37 mass_kg, caliber_m, length_m, "ogive",
38 );
39
40 if spin_inertia.is_finite()
41 && spin_inertia > 0.0
42 && transverse_inertia.is_finite()
43 && transverse_inertia > 0.0
44 {
45 (spin_inertia, transverse_inertia)
46 } else {
47 (0.0, 0.0)
48 }
49}
50
51#[derive(Debug, Clone, Copy)]
53pub struct AngularState {
54 pub pitch_angle: f64, pub yaw_angle: f64, pub pitch_rate: f64, pub yaw_rate: f64, pub precession_angle: f64, pub nutation_phase: f64, }
61
62#[derive(Debug, Clone)]
64pub struct PrecessionNutationParams {
65 pub mass_kg: f64,
67 pub caliber_m: f64,
68 pub length_m: f64,
69 pub spin_rate_rad_s: f64,
70
71 pub spin_inertia: f64, pub transverse_inertia: f64, pub velocity_mps: f64,
77 pub air_density_kg_m3: f64,
78 pub mach: f64,
79
80 pub pitch_damping_coeff: f64,
82 pub nutation_damping_factor: f64, }
84
85impl Default for PrecessionNutationParams {
86 fn default() -> Self {
87 Self {
88 mass_kg: 0.01134, caliber_m: 0.00782,
90 length_m: 0.033,
91 spin_rate_rad_s: 17522.0,
92 spin_inertia: 6.94e-8,
93 transverse_inertia: 9.13e-7,
94 velocity_mps: 850.0,
95 air_density_kg_m3: 1.225,
96 mach: 2.48,
97 pitch_damping_coeff: PitchDampingCoefficients::default().subsonic,
98 nutation_damping_factor: 0.05,
99 }
100 }
101}
102
103pub fn epicyclic_frequencies(
112 spin_inertia: f64,
113 transverse_inertia: f64,
114 spin_rate_rad_s: f64,
115 stability_factor: f64,
116) -> (f64, f64) {
117 if stability_factor <= 1.0 || transverse_inertia == 0.0 {
118 return (0.0, 0.0);
119 }
120 let arm = (spin_inertia * spin_rate_rad_s) / (2.0 * transverse_inertia);
122 let disc = (1.0 - 1.0 / stability_factor).sqrt();
123 (arm * (1.0 + disc), arm * (1.0 - disc)) }
125
126pub fn calculate_precession_frequency(
129 spin_rate_rad_s: f64,
130 spin_inertia: f64,
131 transverse_inertia: f64,
132 stability_factor: f64,
133) -> f64 {
134 epicyclic_frequencies(spin_inertia, transverse_inertia, spin_rate_rad_s, stability_factor).1
135}
136
137pub fn calculate_nutation_frequency(
140 spin_rate_rad_s: f64,
141 spin_inertia: f64,
142 transverse_inertia: f64,
143 stability_factor: f64,
144) -> f64 {
145 epicyclic_frequencies(spin_inertia, transverse_inertia, spin_rate_rad_s, stability_factor).0
146}
147
148pub fn calculate_nutation_amplitude(
150 initial_disturbance_rad: f64,
151 time_s: f64,
152 nutation_frequency: f64,
153 damping_factor: f64,
154 spin_rate_rad_s: f64,
155) -> f64 {
156 if nutation_frequency == 0.0 || spin_rate_rad_s == 0.0 {
157 return 0.0;
158 }
159
160 let damping_rate = damping_factor * nutation_frequency;
162
163 let amplitude = initial_disturbance_rad * (-damping_rate * time_s).exp();
165
166 amplitude.min(0.1) }
169
170pub fn calculate_combined_angular_motion(
172 params: &PrecessionNutationParams,
173 angular_state: &AngularState,
174 time_s: f64,
175 dt: f64,
176 initial_disturbance: f64,
177) -> AngularState {
178 if params.transverse_inertia == 0.0
180 || params.velocity_mps == 0.0
181 || params.length_m == 0.0
182 || !params.air_density_kg_m3.is_finite()
183 || params.air_density_kg_m3 <= 0.0
184 {
185 return *angular_state;
187 }
188
189 let caliber_in = params.caliber_m / 0.0254;
193 let length_in = params.length_m / 0.0254;
194 let mass_gr = params.mass_kg / 0.00006479891;
195 let stability = crate::spin_drift::calculate_dynamic_stability(
196 mass_gr,
197 params.velocity_mps,
198 params.spin_rate_rad_s.abs(),
199 caliber_in,
200 length_in,
201 params.air_density_kg_m3,
202 );
203
204 let omega_p = calculate_precession_frequency(
206 params.spin_rate_rad_s,
207 params.spin_inertia,
208 params.transverse_inertia,
209 stability,
210 );
211 let omega_n = calculate_nutation_frequency(
212 params.spin_rate_rad_s,
213 params.spin_inertia,
214 params.transverse_inertia,
215 stability,
216 );
217
218 let nutation_amp = calculate_nutation_amplitude(
220 initial_disturbance,
221 time_s,
222 omega_n,
223 params.nutation_damping_factor,
224 params.spin_rate_rad_s,
225 );
226
227 let new_precession_angle = angular_state.precession_angle + omega_p * dt;
229
230 let new_nutation_phase = angular_state.nutation_phase + omega_n * dt;
232
233 let pitch_moment = calculate_pitch_damping_moment(
235 angular_state.pitch_rate,
236 params.velocity_mps,
237 params.air_density_kg_m3,
238 params.caliber_m,
239 params.length_m,
240 params.mach,
241 &PitchDampingCoefficients {
242 subsonic: params.pitch_damping_coeff,
243 ..Default::default()
244 },
245 );
246
247 let pitch_accel = pitch_moment / params.transverse_inertia;
249
250 let new_pitch_rate = angular_state.pitch_rate + pitch_accel * dt;
252
253 let coning_amp = initial_disturbance;
259 let total_yaw =
260 coning_amp * new_precession_angle.cos() + nutation_amp * new_nutation_phase.sin();
261 let damping_rate = params.nutation_damping_factor * omega_n;
262 let new_yaw_rate = -coning_amp * omega_p * new_precession_angle.sin()
263 + nutation_amp
264 * (omega_n * new_nutation_phase.cos() - damping_rate * new_nutation_phase.sin());
265
266 let new_pitch = angular_state.pitch_angle + new_pitch_rate * dt;
268
269 AngularState {
270 pitch_angle: new_pitch,
271 yaw_angle: total_yaw,
272 pitch_rate: new_pitch_rate,
273 yaw_rate: new_yaw_rate,
274 precession_angle: new_precession_angle,
275 nutation_phase: new_nutation_phase,
276 }
277}
278
279pub fn calculate_epicyclic_motion(
281 spin_inertia: f64,
282 transverse_inertia: f64,
283 spin_rate_rad_s: f64,
284 stability_factor: f64,
285 time_s: f64,
286 initial_yaw_rad: f64,
287) -> (f64, f64) {
288 if stability_factor <= 1.0 || spin_rate_rad_s == 0.0 {
290 return (initial_yaw_rad, initial_yaw_rad);
292 }
293
294 let (omega_fast, omega_slow) = epicyclic_frequencies(
296 spin_inertia,
297 transverse_inertia,
298 spin_rate_rad_s,
299 stability_factor,
300 );
301
302 let frequency_split = omega_fast - omega_slow;
306 if frequency_split == 0.0 {
307 return (0.0, initial_yaw_rad);
308 }
309 let slow_amplitude = initial_yaw_rad * omega_fast / frequency_split;
310 let initial_fast_amplitude = initial_yaw_rad - slow_amplitude;
311
312 let damping_factor = 0.1; let fast_amplitude = initial_fast_amplitude * (-damping_factor * omega_fast * time_s).exp();
315
316 let slow_phase = omega_slow * time_s;
318 let fast_phase = omega_fast * time_s;
319
320 let yaw = slow_amplitude * slow_phase.cos() + fast_amplitude * fast_phase.cos();
322 let pitch = slow_amplitude * slow_phase.sin() + fast_amplitude * fast_phase.sin();
323
324 (pitch, yaw)
325}
326
327pub fn calculate_limit_cycle_yaw_with_inertias(
334 velocity_mps: f64,
335 spin_rate_rad_s: f64,
336 stability_factor: f64,
337 spin_inertia: f64,
338 transverse_inertia: f64,
339) -> f64 {
340 if !velocity_mps.is_finite()
341 || velocity_mps <= 0.0
342 || !spin_rate_rad_s.is_finite()
343 || spin_rate_rad_s == 0.0
344 || !stability_factor.is_finite()
345 || stability_factor < 1.0
346 || !spin_inertia.is_finite()
347 || spin_inertia <= 0.0
348 || !transverse_inertia.is_finite()
349 || transverse_inertia <= 0.0
350 {
351 return 0.0;
352 }
353
354 4.0 * transverse_inertia * stability_factor * G_ACCEL_MPS2
355 / (spin_inertia * spin_rate_rad_s.abs() * velocity_mps)
356}
357
358#[deprecated(
365 since = "0.22.18",
366 note = "use calculate_limit_cycle_yaw_with_inertias for projectile-specific yaw of repose"
367)]
368pub fn calculate_limit_cycle_yaw(
369 velocity_mps: f64,
370 spin_rate_rad_s: f64,
371 stability_factor: f64,
372 _crosswind_mps: f64,
373) -> f64 {
374 let reference = PrecessionNutationParams::default();
375 calculate_limit_cycle_yaw_with_inertias(
376 velocity_mps,
377 spin_rate_rad_s,
378 stability_factor,
379 reference.spin_inertia,
380 reference.transverse_inertia,
381 )
382}
383
384#[cfg(test)]
385mod tests {
386 use super::*;
387
388 #[test]
389 fn projectile_inertias_match_reference_and_geometry_scaling() {
390 let mass = 0.01134;
391 let caliber = 0.00782;
392 let length = 0.033;
393 let (spin, transverse) = projectile_moments_of_inertia(mass, caliber, length);
394
395 assert!((spin / 6.94e-8 - 1.0).abs() < 0.01);
396 assert!((transverse / 9.13e-7 - 1.0).abs() < 0.01);
397
398 let (double_mass_spin, double_mass_transverse) =
399 projectile_moments_of_inertia(2.0 * mass, caliber, length);
400 assert!((double_mass_spin / spin - 2.0).abs() < 1e-12);
401 assert!((double_mass_transverse / transverse - 2.0).abs() < 1e-12);
402
403 let (double_caliber_spin, double_caliber_transverse) =
404 projectile_moments_of_inertia(mass, 2.0 * caliber, length);
405 assert!((double_caliber_spin / spin - 4.0).abs() < 1e-12);
406 assert!(double_caliber_transverse > transverse);
407
408 let (double_length_spin, double_length_transverse) =
409 projectile_moments_of_inertia(mass, caliber, 2.0 * length);
410 assert!((double_length_spin / spin - 1.0).abs() < 1e-12);
411 assert!(double_length_transverse / transverse > 3.5);
412 }
413
414 #[test]
415 fn projectile_inertias_reject_invalid_geometry() {
416 let invalid = [0.0, -1.0, f64::NAN, f64::INFINITY];
417
418 for value in invalid {
419 assert_eq!(
420 projectile_moments_of_inertia(value, 0.00782, 0.033),
421 (0.0, 0.0)
422 );
423 assert_eq!(
424 projectile_moments_of_inertia(0.01134, value, 0.033),
425 (0.0, 0.0)
426 );
427 assert_eq!(
428 projectile_moments_of_inertia(0.01134, 0.00782, value),
429 (0.0, 0.0)
430 );
431 }
432
433 assert_eq!(
434 projectile_moments_of_inertia(f64::MAX, f64::MAX, f64::MAX),
435 (0.0, 0.0)
436 );
437 }
438
439 #[test]
440 fn test_mba941_epicyclic_relations_and_limits() {
441 let (ix, iy, p) = (6.94e-8_f64, 9.13e-7_f64, 17522.0_f64);
446 let arm = ix * p / (2.0 * iy);
447 for &sg in &[1.5_f64, 2.5, 5.0, 50.0] {
448 let (fast, slow) = epicyclic_frequencies(ix, iy, p, sg);
449 assert!(fast > slow && slow > 0.0, "expect fast>slow>0 at Sg={sg}");
450 assert!(
451 ((fast + slow) - 2.0 * arm).abs() < 1e-6 * arm,
452 "sum != Ix p / Iy at Sg={sg}"
453 );
454 assert!(
455 (fast * slow - arm * arm / sg).abs() < 1e-6 * arm * arm,
456 "product != arm^2 / Sg at Sg={sg}"
457 );
458 }
459 let (f1, s1) = epicyclic_frequencies(ix, iy, p, 1.0 + 1e-9);
461 assert!((f1 - arm).abs() < 1e-3 * arm && (s1 - arm).abs() < 1e-3 * arm);
462 let (f2, s2) = epicyclic_frequencies(ix, iy, p, 1.0e6);
464 assert!(s2 < 1e-3 * arm, "slow precession should vanish at high Sg");
465 assert!((f2 - 2.0 * arm).abs() < 1e-3 * arm, "fast -> Ix p / Iy at high Sg");
466 assert_eq!(epicyclic_frequencies(ix, iy, p, 0.9), (0.0, 0.0));
468 }
469
470 #[test]
471 fn test_precession_frequency() {
472 let freq = calculate_precession_frequency(17522.0, 6.94e-8, 9.13e-7, 2.5);
474 let nut = calculate_nutation_frequency(17522.0, 6.94e-8, 9.13e-7, 2.5);
475 assert!(
477 freq > 0.0 && freq < nut,
478 "precession {freq} should satisfy 0 < freq < nutation {nut}"
479 );
480 assert_eq!(
482 calculate_precession_frequency(17522.0, 6.94e-8, 9.13e-7, 0.9),
483 0.0
484 );
485 }
486
487 #[test]
488 fn test_nutation_frequency() {
489 let freq = calculate_nutation_frequency(17522.0, 6.94e-8, 9.13e-7, 1.5);
492 assert!(
493 (900.0..1200.0).contains(&freq),
494 "nutation freq {freq} rad/s out of expected band"
495 );
496 }
497
498 #[test]
499 fn test_nutation_damping() {
500 let initial = 0.01;
501 let freq = 3000.0;
502
503 let amp_0 = calculate_nutation_amplitude(initial, 0.0, freq, 0.05, 17522.0);
505 let amp_1 = calculate_nutation_amplitude(initial, 0.1, freq, 0.05, 17522.0);
506
507 assert_eq!(amp_0, initial);
508 assert!(amp_1 < amp_0);
509 assert!(amp_1 > 0.0);
510 }
511
512 #[test]
513 fn test_precession_edge_cases() {
514 assert_eq!(
516 calculate_precession_frequency(17522.0, 6.94e-8, 9.13e-7, 0.9),
517 0.0
518 );
519 assert_eq!(
520 calculate_precession_frequency(17522.0, 6.94e-8, 9.13e-7, 1.0),
521 0.0
522 );
523 assert_eq!(
525 calculate_precession_frequency(17522.0, 6.94e-8, 0.0, 2.0),
526 0.0
527 );
528 }
529
530 #[test]
531 fn test_nutation_edge_cases() {
532 let freq_unstable = calculate_nutation_frequency(17522.0, 6.94e-8, 9.13e-7, 0.9);
534 assert_eq!(freq_unstable, 0.0);
535
536 let freq_marginal = calculate_nutation_frequency(17522.0, 6.94e-8, 9.13e-7, 1.0);
538 assert_eq!(freq_marginal, 0.0);
539
540 let freq_zero_inertia = calculate_nutation_frequency(17522.0, 6.94e-8, 0.0, 2.0);
542 assert_eq!(freq_zero_inertia, 0.0);
543 }
544
545 #[test]
546 fn test_nutation_amplitude_bounds() {
547 let initial = 0.5; let freq = 3000.0;
549 let spin = 17522.0;
550
551 let amp = calculate_nutation_amplitude(initial, 0.0, freq, 0.05, spin);
553 assert!(amp <= 0.1); let amp_zero_freq = calculate_nutation_amplitude(initial, 1.0, 0.0, 0.05, spin);
557 assert_eq!(amp_zero_freq, 0.0);
558
559 let amp_zero_spin = calculate_nutation_amplitude(initial, 1.0, freq, 0.05, 0.0);
561 assert_eq!(amp_zero_spin, 0.0);
562 }
563
564 #[test]
565 fn test_epicyclic_motion() {
566 let (pitch, yaw) = calculate_epicyclic_motion(
567 6.94e-8, 9.13e-7, 17522.0, 2.5, 0.1, 0.01, );
574
575 let (omega_fast, omega_slow) = epicyclic_frequencies(6.94e-8, 9.13e-7, 17522.0, 2.5);
577 let frequency_split = omega_fast - omega_slow;
578 let slow_amplitude = 0.01 * omega_fast / frequency_split;
579 let fast_amplitude = 0.01 - slow_amplitude;
580 let bound = slow_amplitude.abs() + fast_amplitude.abs() + 1e-9;
581 assert!(pitch.abs() <= bound, "pitch {pitch} exceeds bound {bound}");
582 assert!(yaw.abs() <= bound, "yaw {yaw} exceeds bound {bound}");
583
584 let (pitch_unstable, yaw_unstable) =
586 calculate_epicyclic_motion(6.94e-8, 9.13e-7, 17522.0, 0.9, 0.1, 0.01);
587 assert_eq!(pitch_unstable, 0.01);
588 assert_eq!(yaw_unstable, 0.01);
589 }
590
591 #[test]
592 fn epicyclic_motion_satisfies_supplied_initial_conditions() {
593 let calculate = |time_s| {
594 calculate_epicyclic_motion(
595 6.94e-8, 9.13e-7, 17522.0, 2.5, time_s, 0.01, )
601 };
602
603 let (initial_pitch, initial_yaw) = calculate(0.0);
604 assert!(initial_pitch.abs() < 1e-15);
605 assert!((initial_yaw - 0.01).abs() < 1e-14);
606
607 let h = 1e-7;
610 let pitch_h = calculate(h).0;
611 let pitch_2h = calculate(2.0 * h).0;
612 let initial_pitch_rate = (4.0 * pitch_h - pitch_2h) / (2.0 * h);
613 assert!(initial_pitch_rate.abs() < 1e-6);
614
615 assert_eq!(
616 calculate_epicyclic_motion(0.0, 9.13e-7, 17522.0, 2.5, 0.1, 0.01),
617 (0.0, 0.01)
618 );
619 assert_eq!(
620 calculate_epicyclic_motion(6.94e-8, 0.0, 17522.0, 2.5, 0.1, 0.01),
621 (0.0, 0.01)
622 );
623 }
624
625 #[test]
626 #[allow(deprecated)]
627 fn limit_cycle_yaw_excludes_crosswind_and_fabricated_instability_step() {
628 let calm = calculate_limit_cycle_yaw(850.0, 17522.0, 2.5, 0.0);
629 let crosswind = calculate_limit_cycle_yaw(850.0, 17522.0, 2.5, 10.0);
630 assert_eq!(crosswind.to_bits(), calm.to_bits());
631 assert!(calm > 0.0);
632
633 let at_boundary = calculate_limit_cycle_yaw(850.0, 17522.0, 1.0, 0.0);
635 let above_boundary = calculate_limit_cycle_yaw(850.0, 17522.0, 1.0_f64.next_up(), 0.0);
636 assert!(at_boundary > 0.0);
637 assert!(((above_boundary - at_boundary) / at_boundary).abs() < 1e-12);
638 assert_eq!(
639 calculate_limit_cycle_yaw(850.0, 17522.0, 1.0_f64.next_down(), 0.0),
640 0.0
641 );
642
643 let low_sg: f64 = 1.1;
646 let high_sg: f64 = 4.0;
647 let low_spin = 10_000.0;
648 let high_spin = low_spin * (high_sg / low_sg).sqrt();
649 let low = calculate_limit_cycle_yaw(850.0, low_spin, low_sg, 0.0);
650 let high = calculate_limit_cycle_yaw(850.0, high_spin, high_sg, 0.0);
651 assert!(high > low);
652 let expected_ratio = (high_sg / low_sg).sqrt();
653 assert!((high / low - expected_ratio).abs() < expected_ratio * 1e-12);
654 }
655
656 #[test]
657 fn inertia_aware_limit_cycle_yaw_matches_gravity_gyroscopic_balance() {
658 let velocity_mps = 850.0;
659 let spin_rate_rad_s = 17522.0;
660 let stability_factor = 2.5;
661 let spin_inertia = 6.94e-8;
662 let transverse_inertia = 9.13e-7;
663 let actual = calculate_limit_cycle_yaw_with_inertias(
664 velocity_mps,
665 spin_rate_rad_s,
666 stability_factor,
667 spin_inertia,
668 transverse_inertia,
669 );
670 let expected = 4.0 * transverse_inertia * stability_factor * 9.80665
671 / (spin_inertia * spin_rate_rad_s * velocity_mps);
672
673 assert!((actual - expected).abs() < 1e-15);
674 assert_eq!(
675 calculate_limit_cycle_yaw_with_inertias(
676 velocity_mps,
677 spin_rate_rad_s,
678 0.9,
679 spin_inertia,
680 transverse_inertia,
681 ),
682 0.0
683 );
684 assert_eq!(
685 calculate_limit_cycle_yaw_with_inertias(
686 velocity_mps,
687 spin_rate_rad_s,
688 stability_factor,
689 0.0,
690 transverse_inertia,
691 ),
692 0.0
693 );
694 }
695
696 #[test]
697 fn test_combined_angular_motion() {
698 let params = PrecessionNutationParams::default();
699 let initial_state = AngularState {
700 pitch_angle: 0.001,
701 yaw_angle: 0.002,
702 pitch_rate: 0.01,
703 yaw_rate: 0.01,
704 precession_angle: 0.0,
705 nutation_phase: 0.0,
706 };
707
708 let new_state = calculate_combined_angular_motion(
709 ¶ms,
710 &initial_state,
711 0.1, 0.001, 0.001, );
715
716 assert!(
719 new_state.nutation_phase != initial_state.nutation_phase
720 || new_state.precession_angle != initial_state.precession_angle
721 );
722
723 assert!(new_state.pitch_angle.abs() < 1.0);
725 assert!(new_state.yaw_angle.abs() < 1.0);
726 }
727
728 #[test]
729 fn combined_motion_yaw_rate_is_derivative_of_yaw() {
730 let params = PrecessionNutationParams::default();
731 let disturbance = 0.001;
732 let time = 0.02;
733 let h = 1e-7;
734 let state = |precession_angle, nutation_phase| AngularState {
735 pitch_angle: 0.0,
736 yaw_angle: 0.0,
737 pitch_rate: 0.0,
738 yaw_rate: 0.0,
739 precession_angle,
740 nutation_phase,
741 };
742
743 let phase_step =
745 calculate_combined_angular_motion(¶ms, &state(0.0, 0.0), time, 1.0, disturbance);
746 let omega_p = phase_step.precession_angle;
747 let omega_n = phase_step.nutation_phase;
748
749 let center_state = state(0.0, std::f64::consts::FRAC_PI_2);
752 let center =
753 calculate_combined_angular_motion(¶ms, ¢er_state, time, 0.0, disturbance);
754 let before_state = state(-omega_p * h, std::f64::consts::FRAC_PI_2 - omega_n * h);
755 let before =
756 calculate_combined_angular_motion(¶ms, &before_state, time - h, 0.0, disturbance);
757 let after_state = state(omega_p * h, std::f64::consts::FRAC_PI_2 + omega_n * h);
758 let after =
759 calculate_combined_angular_motion(¶ms, &after_state, time + h, 0.0, disturbance);
760 let finite_difference = (after.yaw_angle - before.yaw_angle) / (2.0 * h);
761
762 assert!(
763 (center.yaw_rate - finite_difference).abs() < 1e-8,
764 "yaw_rate={} finite_difference={finite_difference}",
765 center.yaw_rate
766 );
767 }
768
769 #[test]
770 fn combined_motion_applies_velocity_and_density_to_stability() {
771 let velocity_mps = 1_000.0;
772 let spin_rate_rad_s = 15_095.0;
773 let params = PrecessionNutationParams {
774 mass_kg: 0.01134,
775 caliber_m: 0.00782,
776 length_m: 0.033,
777 spin_rate_rad_s,
778 spin_inertia: 6.94e-8,
779 transverse_inertia: 9.13e-7,
780 velocity_mps,
781 air_density_kg_m3: 1.0,
782 mach: velocity_mps / 343.0,
783 pitch_damping_coeff: PitchDampingCoefficients::default().subsonic,
784 nutation_damping_factor: 0.05,
785 };
786 let initial_state = AngularState {
787 pitch_angle: 0.0,
788 yaw_angle: 0.0,
789 pitch_rate: 0.0,
790 yaw_rate: 0.0,
791 precession_angle: 0.0,
792 nutation_phase: 0.0,
793 };
794
795 let caliber_in = params.caliber_m / 0.0254;
796 let length_in = params.length_m / 0.0254;
797 let mass_gr = params.mass_kg / 0.00006479891;
798 let spin_rps = spin_rate_rad_s / (2.0 * std::f64::consts::PI);
799 let twist_in = velocity_mps * 3.28084 * 12.0 / spin_rps;
800 let bare_sg = crate::spin_drift::miller_stability(
801 caliber_in,
802 mass_gr,
803 twist_in,
804 length_in,
805 );
806 let velocity_correction = (velocity_mps * 3.28084 / 2800.0).powf(1.0 / 3.0);
807 let density_correction = 1.225 / params.air_density_kg_m3;
808 let corrected_sg = crate::spin_drift::calculate_dynamic_stability(
809 mass_gr,
810 velocity_mps,
811 spin_rate_rad_s,
812 caliber_in,
813 length_in,
814 params.air_density_kg_m3,
815 );
816 assert!(bare_sg < 1.0);
817 assert!(bare_sg * velocity_correction < 1.0);
818 assert!(bare_sg * density_correction < 1.0);
819 assert!(corrected_sg > 1.0);
820
821 let dt = 0.0001;
822 let actual = calculate_combined_angular_motion(¶ms, &initial_state, 0.0, dt, 0.001);
823 let expected_precession = calculate_precession_frequency(
824 spin_rate_rad_s,
825 params.spin_inertia,
826 params.transverse_inertia,
827 corrected_sg,
828 ) * dt;
829 let expected_nutation = calculate_nutation_frequency(
830 spin_rate_rad_s,
831 params.spin_inertia,
832 params.transverse_inertia,
833 corrected_sg,
834 ) * dt;
835
836 assert!(
837 (actual.precession_angle - expected_precession).abs() < 1e-12,
838 "corrected precession phase mismatch: actual={} expected={expected_precession}",
839 actual.precession_angle
840 );
841 assert!(
842 (actual.nutation_phase - expected_nutation).abs() < 1e-12,
843 "corrected nutation phase mismatch: actual={} expected={expected_nutation}",
844 actual.nutation_phase
845 );
846 }
847
848 #[test]
849 fn test_default_params() {
850 let params = PrecessionNutationParams::default();
851
852 assert!(params.mass_kg > 0.0);
854 assert!(params.caliber_m > 0.0);
855 assert!(params.length_m > 0.0);
856 assert!(params.spin_rate_rad_s > 0.0);
857 assert!(params.spin_inertia > 0.0);
858 assert!(params.transverse_inertia > 0.0);
859 assert!(params.velocity_mps > 0.0);
860 assert!(params.air_density_kg_m3 > 0.0);
861 assert!(params.mach > 0.0);
862 assert!(params.nutation_damping_factor > 0.0);
863 assert!(params.nutation_damping_factor < 1.0); }
865
866 #[test]
867 fn test_stability_effects() {
868 let freq_high_stability = calculate_nutation_frequency(17522.0, 6.94e-8, 9.13e-7, 5.0);
871 let freq_low_stability = calculate_nutation_frequency(17522.0, 6.94e-8, 9.13e-7, 1.5);
872 assert!(freq_high_stability > freq_low_stability);
873 }
874
875 #[test]
876 fn test_damping_time_evolution() {
877 let initial = 0.01;
878 let freq = 3000.0;
879 let spin = 17522.0;
880 let damping = 0.1;
881
882 let times = [0.0, 0.01, 0.02, 0.05, 0.1, 0.2];
884 let mut last_amp = initial;
885
886 for &t in ×[1..] {
887 let amp = calculate_nutation_amplitude(initial, t, freq, damping, spin);
888
889 assert!(amp < last_amp);
891 assert!(amp >= 0.0);
892 last_amp = amp;
893 }
894 }
895
896 #[test]
897 fn test_angular_state_evolution() {
898 let params = PrecessionNutationParams {
899 mass_kg: 0.01,
900 caliber_m: 0.008,
901 length_m: 0.03,
902 spin_rate_rad_s: 16000.0, spin_inertia: 5e-8,
904 transverse_inertia: 8e-7,
905 velocity_mps: 800.0,
906 air_density_kg_m3: 1.2,
907 mach: 2.3,
908 pitch_damping_coeff: -5.0,
909 nutation_damping_factor: 0.08,
910 };
911
912 let mut state = AngularState {
913 pitch_angle: 0.0,
914 yaw_angle: 0.005,
915 pitch_rate: 0.0,
916 yaw_rate: 0.0,
917 precession_angle: 0.0,
918 nutation_phase: 0.0,
919 };
920
921 let initial_phase = state.nutation_phase;
923 let initial_precession = state.precession_angle;
924
925 let dt = 0.0001;
927 for i in 0..100 {
928 let time = i as f64 * dt;
929 state = calculate_combined_angular_motion(¶ms, &state, time, dt, 0.002);
930 }
931
932 assert!(
934 state.precession_angle != initial_precession || state.nutation_phase != initial_phase
935 );
936
937 assert!(state.yaw_angle.abs() < 0.1);
939 assert!(state.pitch_angle.abs() < 0.1);
940 }
941}