1use crate::pitch_damping::{calculate_pitch_damping_moment, PitchDampingCoefficients};
12
13#[derive(Debug, Clone, Copy)]
15pub struct AngularState {
16 pub pitch_angle: f64, pub yaw_angle: f64, pub pitch_rate: f64, pub yaw_rate: f64, pub precession_angle: f64, pub nutation_phase: f64, }
23
24#[derive(Debug, Clone)]
26pub struct PrecessionNutationParams {
27 pub mass_kg: f64,
29 pub caliber_m: f64,
30 pub length_m: f64,
31 pub spin_rate_rad_s: f64,
32
33 pub spin_inertia: f64, pub transverse_inertia: f64, pub velocity_mps: f64,
39 pub air_density_kg_m3: f64,
40 pub mach: f64,
41
42 pub pitch_damping_coeff: f64,
44 pub nutation_damping_factor: f64, }
46
47impl Default for PrecessionNutationParams {
48 fn default() -> Self {
49 Self {
50 mass_kg: 0.01134, caliber_m: 0.00782,
52 length_m: 0.033,
53 spin_rate_rad_s: 17522.0,
54 spin_inertia: 6.94e-8,
55 transverse_inertia: 9.13e-7,
56 velocity_mps: 850.0,
57 air_density_kg_m3: 1.225,
58 mach: 2.48,
59 pitch_damping_coeff: -0.8,
60 nutation_damping_factor: 0.05,
61 }
62 }
63}
64
65pub fn epicyclic_frequencies(
74 spin_inertia: f64,
75 transverse_inertia: f64,
76 spin_rate_rad_s: f64,
77 stability_factor: f64,
78) -> (f64, f64) {
79 if stability_factor <= 1.0 || transverse_inertia == 0.0 {
80 return (0.0, 0.0);
81 }
82 let arm = (spin_inertia * spin_rate_rad_s) / (2.0 * transverse_inertia);
84 let disc = (1.0 - 1.0 / stability_factor).sqrt();
85 (arm * (1.0 + disc), arm * (1.0 - disc)) }
87
88pub fn calculate_precession_frequency(
91 spin_rate_rad_s: f64,
92 spin_inertia: f64,
93 transverse_inertia: f64,
94 stability_factor: f64,
95) -> f64 {
96 epicyclic_frequencies(spin_inertia, transverse_inertia, spin_rate_rad_s, stability_factor).1
97}
98
99pub fn calculate_nutation_frequency(
102 spin_rate_rad_s: f64,
103 spin_inertia: f64,
104 transverse_inertia: f64,
105 stability_factor: f64,
106) -> f64 {
107 epicyclic_frequencies(spin_inertia, transverse_inertia, spin_rate_rad_s, stability_factor).0
108}
109
110pub fn calculate_nutation_amplitude(
112 initial_disturbance_rad: f64,
113 time_s: f64,
114 nutation_frequency: f64,
115 damping_factor: f64,
116 spin_rate_rad_s: f64,
117) -> f64 {
118 if nutation_frequency == 0.0 || spin_rate_rad_s == 0.0 {
119 return 0.0;
120 }
121
122 let damping_rate = damping_factor * nutation_frequency;
124
125 let amplitude = initial_disturbance_rad * (-damping_rate * time_s).exp();
127
128 amplitude.min(0.1) }
131
132pub fn calculate_combined_angular_motion(
134 params: &PrecessionNutationParams,
135 angular_state: &AngularState,
136 time_s: f64,
137 dt: f64,
138 initial_disturbance: f64,
139) -> AngularState {
140 if params.transverse_inertia == 0.0 || params.velocity_mps == 0.0 || params.length_m == 0.0 {
142 return *angular_state;
144 }
145
146 let caliber_in = params.caliber_m / 0.0254;
150 let length_in = params.length_m / 0.0254;
151 let mass_gr = params.mass_kg / 0.00006479891;
152 let twist_in = if params.spin_rate_rad_s.abs() > 1e-9 {
153 (2.0 * std::f64::consts::PI * params.velocity_mps / params.spin_rate_rad_s).abs() / 0.0254
154 } else {
155 0.0
156 };
157 let stability = crate::spin_drift::miller_stability(caliber_in, mass_gr, twist_in, length_in);
158
159 let omega_p = calculate_precession_frequency(
161 params.spin_rate_rad_s,
162 params.spin_inertia,
163 params.transverse_inertia,
164 stability,
165 );
166 let omega_n = calculate_nutation_frequency(
167 params.spin_rate_rad_s,
168 params.spin_inertia,
169 params.transverse_inertia,
170 stability,
171 );
172
173 let nutation_amp = calculate_nutation_amplitude(
175 initial_disturbance,
176 time_s,
177 omega_n,
178 params.nutation_damping_factor,
179 params.spin_rate_rad_s,
180 );
181
182 let new_precession_angle = angular_state.precession_angle + omega_p * dt;
184
185 let new_nutation_phase = angular_state.nutation_phase + omega_n * dt;
187
188 let pitch_moment = calculate_pitch_damping_moment(
190 angular_state.pitch_rate,
191 params.velocity_mps,
192 params.air_density_kg_m3,
193 params.caliber_m,
194 params.length_m,
195 params.mach,
196 &PitchDampingCoefficients {
197 subsonic: params.pitch_damping_coeff,
198 ..Default::default()
199 },
200 );
201
202 let pitch_accel = pitch_moment / params.transverse_inertia;
204
205 let new_pitch_rate = angular_state.pitch_rate + pitch_accel * dt;
207
208 let coning_amp = initial_disturbance;
214 let total_yaw =
215 coning_amp * new_precession_angle.cos() + nutation_amp * new_nutation_phase.sin();
216 let new_yaw_rate = -coning_amp * omega_p * new_precession_angle.sin()
217 + nutation_amp * omega_n * new_nutation_phase.cos();
218
219 let new_pitch = angular_state.pitch_angle + new_pitch_rate * dt;
221
222 AngularState {
223 pitch_angle: new_pitch,
224 yaw_angle: total_yaw,
225 pitch_rate: new_pitch_rate,
226 yaw_rate: new_yaw_rate,
227 precession_angle: new_precession_angle,
228 nutation_phase: new_nutation_phase,
229 }
230}
231
232pub fn calculate_epicyclic_motion(
234 spin_inertia: f64,
235 transverse_inertia: f64,
236 spin_rate_rad_s: f64,
237 stability_factor: f64,
238 time_s: f64,
239 initial_yaw_rad: f64,
240) -> (f64, f64) {
241 if stability_factor <= 1.0 || spin_rate_rad_s == 0.0 {
243 return (initial_yaw_rad, initial_yaw_rad);
245 }
246
247 let (omega_fast, omega_slow) =
249 epicyclic_frequencies(spin_inertia, transverse_inertia, spin_rate_rad_s, stability_factor);
250
251 let amplitude_ratio = 1.0 / stability_factor;
253
254 let damping_factor = 0.1; let fast_amplitude = amplitude_ratio * (-damping_factor * omega_fast * time_s).exp();
257
258 let slow_phase = omega_slow * time_s;
260 let fast_phase = omega_fast * time_s;
261
262 let yaw = initial_yaw_rad * (slow_phase.cos() + fast_amplitude * fast_phase.cos());
264 let pitch = initial_yaw_rad * (slow_phase.sin() + fast_amplitude * fast_phase.sin());
265
266 (pitch, yaw)
267}
268
269pub fn calculate_limit_cycle_yaw(
271 velocity_mps: f64,
272 _spin_rate_rad_s: f64,
273 stability_factor: f64,
274 crosswind_mps: f64,
275) -> f64 {
276 let wind_yaw = if crosswind_mps != 0.0 && velocity_mps > 0.0 {
278 (crosswind_mps / velocity_mps).atan()
279 } else {
280 0.0
281 };
282
283 let yaw_of_repose = if stability_factor > 1.0 {
285 let repose_factor = 1.0 / (1.0 + 0.5 * (stability_factor - 1.0));
287 0.002 * repose_factor } else {
289 0.01 };
291
292 wind_yaw + yaw_of_repose
293}
294
295#[cfg(test)]
296mod tests {
297 use super::*;
298
299 #[test]
300 fn test_mba941_epicyclic_relations_and_limits() {
301 let (ix, iy, p) = (6.94e-8_f64, 9.13e-7_f64, 17522.0_f64);
306 let arm = ix * p / (2.0 * iy);
307 for &sg in &[1.5_f64, 2.5, 5.0, 50.0] {
308 let (fast, slow) = epicyclic_frequencies(ix, iy, p, sg);
309 assert!(fast > slow && slow > 0.0, "expect fast>slow>0 at Sg={sg}");
310 assert!(
311 ((fast + slow) - 2.0 * arm).abs() < 1e-6 * arm,
312 "sum != Ix p / Iy at Sg={sg}"
313 );
314 assert!(
315 (fast * slow - arm * arm / sg).abs() < 1e-6 * arm * arm,
316 "product != arm^2 / Sg at Sg={sg}"
317 );
318 }
319 let (f1, s1) = epicyclic_frequencies(ix, iy, p, 1.0 + 1e-9);
321 assert!((f1 - arm).abs() < 1e-3 * arm && (s1 - arm).abs() < 1e-3 * arm);
322 let (f2, s2) = epicyclic_frequencies(ix, iy, p, 1.0e6);
324 assert!(s2 < 1e-3 * arm, "slow precession should vanish at high Sg");
325 assert!((f2 - 2.0 * arm).abs() < 1e-3 * arm, "fast -> Ix p / Iy at high Sg");
326 assert_eq!(epicyclic_frequencies(ix, iy, p, 0.9), (0.0, 0.0));
328 }
329
330 #[test]
331 fn test_precession_frequency() {
332 let freq = calculate_precession_frequency(17522.0, 6.94e-8, 9.13e-7, 2.5);
334 let nut = calculate_nutation_frequency(17522.0, 6.94e-8, 9.13e-7, 2.5);
335 assert!(
337 freq > 0.0 && freq < nut,
338 "precession {freq} should satisfy 0 < freq < nutation {nut}"
339 );
340 assert_eq!(
342 calculate_precession_frequency(17522.0, 6.94e-8, 9.13e-7, 0.9),
343 0.0
344 );
345 }
346
347 #[test]
348 fn test_nutation_frequency() {
349 let freq = calculate_nutation_frequency(17522.0, 6.94e-8, 9.13e-7, 1.5);
352 assert!(
353 (900.0..1200.0).contains(&freq),
354 "nutation freq {freq} rad/s out of expected band"
355 );
356 }
357
358 #[test]
359 fn test_nutation_damping() {
360 let initial = 0.01;
361 let freq = 3000.0;
362
363 let amp_0 = calculate_nutation_amplitude(initial, 0.0, freq, 0.05, 17522.0);
365 let amp_1 = calculate_nutation_amplitude(initial, 0.1, freq, 0.05, 17522.0);
366
367 assert_eq!(amp_0, initial);
368 assert!(amp_1 < amp_0);
369 assert!(amp_1 > 0.0);
370 }
371
372 #[test]
373 fn test_precession_edge_cases() {
374 assert_eq!(
376 calculate_precession_frequency(17522.0, 6.94e-8, 9.13e-7, 0.9),
377 0.0
378 );
379 assert_eq!(
380 calculate_precession_frequency(17522.0, 6.94e-8, 9.13e-7, 1.0),
381 0.0
382 );
383 assert_eq!(
385 calculate_precession_frequency(17522.0, 6.94e-8, 0.0, 2.0),
386 0.0
387 );
388 }
389
390 #[test]
391 fn test_nutation_edge_cases() {
392 let freq_unstable = calculate_nutation_frequency(17522.0, 6.94e-8, 9.13e-7, 0.9);
394 assert_eq!(freq_unstable, 0.0);
395
396 let freq_marginal = calculate_nutation_frequency(17522.0, 6.94e-8, 9.13e-7, 1.0);
398 assert_eq!(freq_marginal, 0.0);
399
400 let freq_zero_inertia = calculate_nutation_frequency(17522.0, 6.94e-8, 0.0, 2.0);
402 assert_eq!(freq_zero_inertia, 0.0);
403 }
404
405 #[test]
406 fn test_nutation_amplitude_bounds() {
407 let initial = 0.5; let freq = 3000.0;
409 let spin = 17522.0;
410
411 let amp = calculate_nutation_amplitude(initial, 0.0, freq, 0.05, spin);
413 assert!(amp <= 0.1); let amp_zero_freq = calculate_nutation_amplitude(initial, 1.0, 0.0, 0.05, spin);
417 assert_eq!(amp_zero_freq, 0.0);
418
419 let amp_zero_spin = calculate_nutation_amplitude(initial, 1.0, freq, 0.05, 0.0);
421 assert_eq!(amp_zero_spin, 0.0);
422 }
423
424 #[test]
425 fn test_epicyclic_motion() {
426 let (pitch, yaw) = calculate_epicyclic_motion(
427 6.94e-8, 9.13e-7, 17522.0, 2.5, 0.1, 0.01, );
434
435 let bound = 0.01 * (1.0 + 1.0 / 2.5) + 1e-9;
437 assert!(pitch.abs() <= bound, "pitch {pitch} exceeds bound {bound}");
438 assert!(yaw.abs() <= bound, "yaw {yaw} exceeds bound {bound}");
439
440 let (pitch_unstable, yaw_unstable) =
442 calculate_epicyclic_motion(6.94e-8, 9.13e-7, 17522.0, 0.9, 0.1, 0.01);
443 assert_eq!(pitch_unstable, 0.01);
444 assert_eq!(yaw_unstable, 0.01);
445 }
446
447 #[test]
448 fn test_limit_cycle_yaw() {
449 let yaw_wind = calculate_limit_cycle_yaw(
451 850.0, 17522.0, 2.5, 10.0, );
456
457 assert!(yaw_wind > 0.0);
459 assert!(yaw_wind < 0.1);
460
461 let yaw_no_wind = calculate_limit_cycle_yaw(850.0, 17522.0, 2.5, 0.0);
463 assert!(yaw_no_wind > 0.0);
464 assert!(yaw_no_wind < yaw_wind);
465
466 let yaw_unstable = calculate_limit_cycle_yaw(850.0, 17522.0, 0.9, 0.0);
468 assert_eq!(yaw_unstable, 0.01); }
470
471 #[test]
472 fn test_combined_angular_motion() {
473 let params = PrecessionNutationParams::default();
474 let initial_state = AngularState {
475 pitch_angle: 0.001,
476 yaw_angle: 0.002,
477 pitch_rate: 0.01,
478 yaw_rate: 0.01,
479 precession_angle: 0.0,
480 nutation_phase: 0.0,
481 };
482
483 let new_state = calculate_combined_angular_motion(
484 ¶ms,
485 &initial_state,
486 0.1, 0.001, 0.001, );
490
491 assert!(
494 new_state.nutation_phase != initial_state.nutation_phase
495 || new_state.precession_angle != initial_state.precession_angle
496 );
497
498 assert!(new_state.pitch_angle.abs() < 1.0);
500 assert!(new_state.yaw_angle.abs() < 1.0);
501 }
502
503 #[test]
504 fn test_default_params() {
505 let params = PrecessionNutationParams::default();
506
507 assert!(params.mass_kg > 0.0);
509 assert!(params.caliber_m > 0.0);
510 assert!(params.length_m > 0.0);
511 assert!(params.spin_rate_rad_s > 0.0);
512 assert!(params.spin_inertia > 0.0);
513 assert!(params.transverse_inertia > 0.0);
514 assert!(params.velocity_mps > 0.0);
515 assert!(params.air_density_kg_m3 > 0.0);
516 assert!(params.mach > 0.0);
517 assert!(params.nutation_damping_factor > 0.0);
518 assert!(params.nutation_damping_factor < 1.0); }
520
521 #[test]
522 fn test_stability_effects() {
523 let freq_high_stability = calculate_nutation_frequency(17522.0, 6.94e-8, 9.13e-7, 5.0);
526 let freq_low_stability = calculate_nutation_frequency(17522.0, 6.94e-8, 9.13e-7, 1.5);
527 assert!(freq_high_stability > freq_low_stability);
528 }
529
530 #[test]
531 fn test_damping_time_evolution() {
532 let initial = 0.01;
533 let freq = 3000.0;
534 let spin = 17522.0;
535 let damping = 0.1;
536
537 let times = [0.0, 0.01, 0.02, 0.05, 0.1, 0.2];
539 let mut last_amp = initial;
540
541 for &t in ×[1..] {
542 let amp = calculate_nutation_amplitude(initial, t, freq, damping, spin);
543
544 assert!(amp < last_amp);
546 assert!(amp >= 0.0);
547 last_amp = amp;
548 }
549 }
550
551 #[test]
552 fn test_angular_state_evolution() {
553 let params = PrecessionNutationParams {
554 mass_kg: 0.01,
555 caliber_m: 0.008,
556 length_m: 0.03,
557 spin_rate_rad_s: 16000.0, spin_inertia: 5e-8,
559 transverse_inertia: 8e-7,
560 velocity_mps: 800.0,
561 air_density_kg_m3: 1.2,
562 mach: 2.3,
563 pitch_damping_coeff: -0.5,
564 nutation_damping_factor: 0.08,
565 };
566
567 let mut state = AngularState {
568 pitch_angle: 0.0,
569 yaw_angle: 0.005,
570 pitch_rate: 0.0,
571 yaw_rate: 0.0,
572 precession_angle: 0.0,
573 nutation_phase: 0.0,
574 };
575
576 let initial_phase = state.nutation_phase;
578 let initial_precession = state.precession_angle;
579
580 let dt = 0.0001;
582 for i in 0..100 {
583 let time = i as f64 * dt;
584 state = calculate_combined_angular_motion(¶ms, &state, time, dt, 0.002);
585 }
586
587 assert!(
589 state.precession_angle != initial_precession || state.nutation_phase != initial_phase
590 );
591
592 assert!(state.yaw_angle.abs() < 0.1);
594 assert!(state.pitch_angle.abs() < 0.1);
595 }
596}