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 calculate_precession_frequency(
67 spin_rate_rad_s: f64,
68 velocity_mps: f64,
69 spin_inertia: f64,
70 transverse_inertia: f64,
71 yaw_angle_rad: f64,
72) -> f64 {
73 if velocity_mps == 0.0 || transverse_inertia == 0.0 {
74 return 0.0;
75 }
76
77 (spin_inertia * spin_rate_rad_s * yaw_angle_rad.sin()) / (transverse_inertia * velocity_mps)
80}
81
82pub fn calculate_nutation_frequency(
84 spin_rate_rad_s: f64,
85 spin_inertia: f64,
86 transverse_inertia: f64,
87 stability_factor: f64,
88) -> f64 {
89 if stability_factor <= 1.0 || transverse_inertia == 0.0 {
90 return 0.0;
91 }
92
93 let inertia_ratio = spin_inertia / transverse_inertia;
96 spin_rate_rad_s * inertia_ratio.sqrt() * (stability_factor - 1.0).sqrt()
97}
98
99pub fn calculate_nutation_amplitude(
101 initial_disturbance_rad: f64,
102 time_s: f64,
103 nutation_frequency: f64,
104 damping_factor: f64,
105 spin_rate_rad_s: f64,
106) -> f64 {
107 if nutation_frequency == 0.0 || spin_rate_rad_s == 0.0 {
108 return 0.0;
109 }
110
111 let damping_rate = damping_factor * nutation_frequency;
113
114 let amplitude = initial_disturbance_rad * (-damping_rate * time_s).exp();
116
117 amplitude.min(0.1) }
120
121pub fn calculate_combined_angular_motion(
123 params: &PrecessionNutationParams,
124 angular_state: &AngularState,
125 time_s: f64,
126 dt: f64,
127 initial_disturbance: f64,
128) -> AngularState {
129 if params.transverse_inertia == 0.0 || params.velocity_mps == 0.0 || params.length_m == 0.0 {
131 return *angular_state;
133 }
134
135 let stability = (params.spin_inertia * params.spin_rate_rad_s.powi(2))
137 / (4.0 * params.transverse_inertia * params.velocity_mps.powi(2) / params.length_m);
138
139 let omega_p = calculate_precession_frequency(
141 params.spin_rate_rad_s,
142 params.velocity_mps,
143 params.spin_inertia,
144 params.transverse_inertia,
145 angular_state.yaw_angle,
146 );
147
148 let omega_n = calculate_nutation_frequency(
150 params.spin_rate_rad_s,
151 params.spin_inertia,
152 params.transverse_inertia,
153 stability,
154 );
155
156 let nutation_amp = calculate_nutation_amplitude(
158 initial_disturbance,
159 time_s,
160 omega_n,
161 params.nutation_damping_factor,
162 params.spin_rate_rad_s,
163 );
164
165 let new_precession_angle = angular_state.precession_angle + omega_p * dt;
167
168 let new_nutation_phase = angular_state.nutation_phase + omega_n * dt;
170
171 let pitch_moment = calculate_pitch_damping_moment(
173 angular_state.pitch_rate,
174 params.velocity_mps,
175 params.air_density_kg_m3,
176 params.caliber_m,
177 params.length_m,
178 params.mach,
179 &PitchDampingCoefficients {
180 subsonic: params.pitch_damping_coeff,
181 ..Default::default()
182 },
183 );
184
185 let pitch_accel = pitch_moment / params.transverse_inertia;
187
188 let new_pitch_rate = angular_state.pitch_rate + pitch_accel * dt;
190 let new_yaw_rate = omega_p; let total_yaw = angular_state.yaw_angle + nutation_amp * new_nutation_phase.sin();
195
196 let new_pitch = angular_state.pitch_angle + new_pitch_rate * dt;
198
199 AngularState {
200 pitch_angle: new_pitch,
201 yaw_angle: total_yaw,
202 pitch_rate: new_pitch_rate,
203 yaw_rate: new_yaw_rate,
204 precession_angle: new_precession_angle,
205 nutation_phase: new_nutation_phase,
206 }
207}
208
209pub fn calculate_epicyclic_motion(
211 spin_rate_rad_s: f64,
212 velocity_mps: f64,
213 stability_factor: f64,
214 time_s: f64,
215 initial_yaw_rad: f64,
216) -> (f64, f64) {
217 if stability_factor <= 1.0 || spin_rate_rad_s == 0.0 {
219 return (initial_yaw_rad, initial_yaw_rad);
221 }
222
223 let omega_slow = 2.0 * velocity_mps / (stability_factor * spin_rate_rad_s);
226
227 let omega_fast = spin_rate_rad_s * ((stability_factor - 1.0).sqrt()) / stability_factor;
229
230 let amplitude_ratio = 1.0 / stability_factor;
232
233 let damping_factor = 0.1; let fast_amplitude = amplitude_ratio * (-damping_factor * omega_fast * time_s).exp();
236
237 let slow_phase = omega_slow * time_s;
239 let fast_phase = omega_fast * time_s;
240
241 let yaw = initial_yaw_rad * (slow_phase.cos() + fast_amplitude * fast_phase.cos());
243 let pitch = initial_yaw_rad * (slow_phase.sin() + fast_amplitude * fast_phase.sin());
244
245 (pitch, yaw)
246}
247
248pub fn calculate_limit_cycle_yaw(
250 velocity_mps: f64,
251 _spin_rate_rad_s: f64,
252 stability_factor: f64,
253 crosswind_mps: f64,
254) -> f64 {
255 let wind_yaw = if crosswind_mps != 0.0 && velocity_mps > 0.0 {
257 (crosswind_mps / velocity_mps).atan()
258 } else {
259 0.0
260 };
261
262 let yaw_of_repose = if stability_factor > 1.0 {
264 let repose_factor = 1.0 / (1.0 + 0.5 * (stability_factor - 1.0));
266 0.002 * repose_factor } else {
268 0.01 };
270
271 wind_yaw + yaw_of_repose
272}
273
274#[cfg(test)]
275mod tests {
276 use super::*;
277
278 #[test]
279 fn test_precession_frequency() {
280 let freq = calculate_precession_frequency(
281 17522.0, 850.0, 6.94e-8, 9.13e-7, 0.002, );
287
288 assert!(freq.abs() < 1.0);
290 }
291
292 #[test]
293 fn test_nutation_frequency() {
294 let freq = calculate_nutation_frequency(
295 17522.0, 6.94e-8, 9.13e-7, 1.5, );
300
301 assert!(freq > 1000.0);
303 assert!(freq < 10000.0);
304 }
305
306 #[test]
307 fn test_nutation_damping() {
308 let initial = 0.01;
309 let freq = 3000.0;
310
311 let amp_0 = calculate_nutation_amplitude(initial, 0.0, freq, 0.05, 17522.0);
313 let amp_1 = calculate_nutation_amplitude(initial, 0.1, freq, 0.05, 17522.0);
314
315 assert_eq!(amp_0, initial);
316 assert!(amp_1 < amp_0);
317 assert!(amp_1 > 0.0);
318 }
319
320 #[test]
321 fn test_precession_edge_cases() {
322 let freq_zero_vel = calculate_precession_frequency(17522.0, 0.0, 6.94e-8, 9.13e-7, 0.002);
324 assert_eq!(freq_zero_vel, 0.0);
325
326 let freq_zero_inertia = calculate_precession_frequency(17522.0, 850.0, 6.94e-8, 0.0, 0.002);
328 assert_eq!(freq_zero_inertia, 0.0);
329
330 let freq_zero_yaw = calculate_precession_frequency(17522.0, 850.0, 6.94e-8, 9.13e-7, 0.0);
332 assert_eq!(freq_zero_yaw, 0.0);
333 }
334
335 #[test]
336 fn test_nutation_edge_cases() {
337 let freq_unstable = calculate_nutation_frequency(17522.0, 6.94e-8, 9.13e-7, 0.9);
339 assert_eq!(freq_unstable, 0.0);
340
341 let freq_marginal = calculate_nutation_frequency(17522.0, 6.94e-8, 9.13e-7, 1.0);
343 assert_eq!(freq_marginal, 0.0);
344
345 let freq_zero_inertia = calculate_nutation_frequency(17522.0, 6.94e-8, 0.0, 2.0);
347 assert_eq!(freq_zero_inertia, 0.0);
348 }
349
350 #[test]
351 fn test_nutation_amplitude_bounds() {
352 let initial = 0.5; let freq = 3000.0;
354 let spin = 17522.0;
355
356 let amp = calculate_nutation_amplitude(initial, 0.0, freq, 0.05, spin);
358 assert!(amp <= 0.1); let amp_zero_freq = calculate_nutation_amplitude(initial, 1.0, 0.0, 0.05, spin);
362 assert_eq!(amp_zero_freq, 0.0);
363
364 let amp_zero_spin = calculate_nutation_amplitude(initial, 1.0, freq, 0.05, 0.0);
366 assert_eq!(amp_zero_spin, 0.0);
367 }
368
369 #[test]
370 fn test_epicyclic_motion() {
371 let (pitch, yaw) = calculate_epicyclic_motion(
372 17522.0, 850.0, 2.5, 0.1, 0.01, );
378
379 assert!(pitch.abs() <= 0.01);
381 assert!(yaw.abs() <= 0.01);
382
383 let (pitch_unstable, yaw_unstable) =
385 calculate_epicyclic_motion(17522.0, 850.0, 0.9, 0.1, 0.01);
386 assert_eq!(pitch_unstable, 0.01);
387 assert_eq!(yaw_unstable, 0.01);
388 }
389
390 #[test]
391 fn test_limit_cycle_yaw() {
392 let yaw_wind = calculate_limit_cycle_yaw(
394 850.0, 17522.0, 2.5, 10.0, );
399
400 assert!(yaw_wind > 0.0);
402 assert!(yaw_wind < 0.1);
403
404 let yaw_no_wind = calculate_limit_cycle_yaw(850.0, 17522.0, 2.5, 0.0);
406 assert!(yaw_no_wind > 0.0);
407 assert!(yaw_no_wind < yaw_wind);
408
409 let yaw_unstable = calculate_limit_cycle_yaw(850.0, 17522.0, 0.9, 0.0);
411 assert_eq!(yaw_unstable, 0.01); }
413
414 #[test]
415 fn test_combined_angular_motion() {
416 let params = PrecessionNutationParams::default();
417 let initial_state = AngularState {
418 pitch_angle: 0.001,
419 yaw_angle: 0.002,
420 pitch_rate: 0.01,
421 yaw_rate: 0.01,
422 precession_angle: 0.0,
423 nutation_phase: 0.0,
424 };
425
426 let new_state = calculate_combined_angular_motion(
427 ¶ms,
428 &initial_state,
429 0.1, 0.001, 0.001, );
433
434 assert!(
437 new_state.nutation_phase != initial_state.nutation_phase
438 || new_state.precession_angle != initial_state.precession_angle
439 );
440
441 assert!(new_state.pitch_angle.abs() < 1.0);
443 assert!(new_state.yaw_angle.abs() < 1.0);
444 }
445
446 #[test]
447 fn test_default_params() {
448 let params = PrecessionNutationParams::default();
449
450 assert!(params.mass_kg > 0.0);
452 assert!(params.caliber_m > 0.0);
453 assert!(params.length_m > 0.0);
454 assert!(params.spin_rate_rad_s > 0.0);
455 assert!(params.spin_inertia > 0.0);
456 assert!(params.transverse_inertia > 0.0);
457 assert!(params.velocity_mps > 0.0);
458 assert!(params.air_density_kg_m3 > 0.0);
459 assert!(params.mach > 0.0);
460 assert!(params.nutation_damping_factor > 0.0);
461 assert!(params.nutation_damping_factor < 1.0); }
463
464 #[test]
465 fn test_stability_effects() {
466 let freq_high_stability = calculate_nutation_frequency(17522.0, 6.94e-8, 9.13e-7, 5.0);
468
469 let freq_low_stability = calculate_nutation_frequency(17522.0, 6.94e-8, 9.13e-7, 1.5);
470
471 assert!(freq_high_stability > freq_low_stability);
473 }
474
475 #[test]
476 fn test_damping_time_evolution() {
477 let initial = 0.01;
478 let freq = 3000.0;
479 let spin = 17522.0;
480 let damping = 0.1;
481
482 let times = [0.0, 0.01, 0.02, 0.05, 0.1, 0.2];
484 let mut last_amp = initial;
485
486 for &t in ×[1..] {
487 let amp = calculate_nutation_amplitude(initial, t, freq, damping, spin);
488
489 assert!(amp < last_amp);
491 assert!(amp >= 0.0);
492 last_amp = amp;
493 }
494 }
495
496 #[test]
497 fn test_angular_state_evolution() {
498 let params = PrecessionNutationParams {
499 mass_kg: 0.01,
500 caliber_m: 0.008,
501 length_m: 0.03,
502 spin_rate_rad_s: 10000.0,
503 spin_inertia: 5e-8,
504 transverse_inertia: 8e-7,
505 velocity_mps: 800.0,
506 air_density_kg_m3: 1.2,
507 mach: 2.3,
508 pitch_damping_coeff: -0.5,
509 nutation_damping_factor: 0.08,
510 };
511
512 let mut state = AngularState {
513 pitch_angle: 0.0,
514 yaw_angle: 0.005,
515 pitch_rate: 0.0,
516 yaw_rate: 0.0,
517 precession_angle: 0.0,
518 nutation_phase: 0.0,
519 };
520
521 let initial_phase = state.nutation_phase;
523 let initial_precession = state.precession_angle;
524
525 let dt = 0.0001;
527 for i in 0..100 {
528 let time = i as f64 * dt;
529 state = calculate_combined_angular_motion(¶ms, &state, time, dt, 0.002);
530 }
531
532 assert!(
534 state.precession_angle != initial_precession || state.nutation_phase != initial_phase
535 );
536
537 assert!(state.yaw_angle.abs() < 0.1);
539 assert!(state.pitch_angle.abs() < 0.1);
540 }
541}