1use crate::pitch_damping::{
12 calculate_pitch_damping_moment,
13 PitchDampingCoefficients,
14};
15
16#[derive(Debug, Clone, Copy)]
18pub struct AngularState {
19 pub pitch_angle: f64, pub yaw_angle: f64, pub pitch_rate: f64, pub yaw_rate: f64, pub precession_angle: f64, pub nutation_phase: f64, }
26
27#[derive(Debug, Clone)]
29pub struct PrecessionNutationParams {
30 pub mass_kg: f64,
32 pub caliber_m: f64,
33 pub length_m: f64,
34 pub spin_rate_rad_s: f64,
35
36 pub spin_inertia: f64, pub transverse_inertia: f64, pub velocity_mps: f64,
42 pub air_density_kg_m3: f64,
43 pub mach: f64,
44
45 pub pitch_damping_coeff: f64,
47 pub nutation_damping_factor: f64, }
49
50impl Default for PrecessionNutationParams {
51 fn default() -> Self {
52 Self {
53 mass_kg: 0.01134, caliber_m: 0.00782,
55 length_m: 0.033,
56 spin_rate_rad_s: 17522.0,
57 spin_inertia: 6.94e-8,
58 transverse_inertia: 9.13e-7,
59 velocity_mps: 850.0,
60 air_density_kg_m3: 1.225,
61 mach: 2.48,
62 pitch_damping_coeff: -0.8,
63 nutation_damping_factor: 0.05,
64 }
65 }
66}
67
68pub fn calculate_precession_frequency(
70 spin_rate_rad_s: f64,
71 velocity_mps: f64,
72 spin_inertia: f64,
73 transverse_inertia: f64,
74 yaw_angle_rad: f64,
75) -> f64 {
76 if velocity_mps == 0.0 || transverse_inertia == 0.0 {
77 return 0.0;
78 }
79
80 (spin_inertia * spin_rate_rad_s * yaw_angle_rad.sin()) /
83 (transverse_inertia * velocity_mps)
84}
85
86pub fn calculate_nutation_frequency(
88 spin_rate_rad_s: f64,
89 spin_inertia: f64,
90 transverse_inertia: f64,
91 stability_factor: f64,
92) -> f64 {
93 if stability_factor <= 1.0 || transverse_inertia == 0.0 {
94 return 0.0;
95 }
96
97 let inertia_ratio = spin_inertia / transverse_inertia;
100 spin_rate_rad_s * inertia_ratio.sqrt() * (stability_factor - 1.0).sqrt()
101}
102
103pub fn calculate_nutation_amplitude(
105 initial_disturbance_rad: f64,
106 time_s: f64,
107 nutation_frequency: f64,
108 damping_factor: f64,
109 spin_rate_rad_s: f64,
110) -> f64 {
111 if nutation_frequency == 0.0 || spin_rate_rad_s == 0.0 {
112 return 0.0;
113 }
114
115 let damping_rate = damping_factor * nutation_frequency;
117
118 let amplitude = initial_disturbance_rad * (-damping_rate * time_s).exp();
120
121 amplitude.min(0.1) }
124
125pub fn calculate_combined_angular_motion(
127 params: &PrecessionNutationParams,
128 angular_state: &AngularState,
129 time_s: f64,
130 dt: f64,
131 initial_disturbance: f64,
132) -> AngularState {
133 let stability = (params.spin_inertia * params.spin_rate_rad_s.powi(2)) /
135 (4.0 * params.transverse_inertia * params.velocity_mps.powi(2) / params.length_m);
136
137 let omega_p = calculate_precession_frequency(
139 params.spin_rate_rad_s,
140 params.velocity_mps,
141 params.spin_inertia,
142 params.transverse_inertia,
143 angular_state.yaw_angle,
144 );
145
146 let omega_n = calculate_nutation_frequency(
148 params.spin_rate_rad_s,
149 params.spin_inertia,
150 params.transverse_inertia,
151 stability,
152 );
153
154 let nutation_amp = calculate_nutation_amplitude(
156 initial_disturbance,
157 time_s,
158 omega_n,
159 params.nutation_damping_factor,
160 params.spin_rate_rad_s,
161 );
162
163 let new_precession_angle = angular_state.precession_angle + omega_p * dt;
165
166 let new_nutation_phase = angular_state.nutation_phase + omega_n * dt;
168
169 let pitch_moment = calculate_pitch_damping_moment(
171 angular_state.pitch_rate,
172 params.velocity_mps,
173 params.air_density_kg_m3,
174 params.caliber_m,
175 params.length_m,
176 params.mach,
177 &PitchDampingCoefficients {
178 subsonic: params.pitch_damping_coeff,
179 ..Default::default()
180 },
181 );
182
183 let pitch_accel = pitch_moment / params.transverse_inertia;
185
186 let new_pitch_rate = angular_state.pitch_rate + pitch_accel * dt;
188 let new_yaw_rate = omega_p; let total_yaw = angular_state.yaw_angle + nutation_amp * new_nutation_phase.sin();
193
194 let new_pitch = angular_state.pitch_angle + new_pitch_rate * dt;
196
197 AngularState {
198 pitch_angle: new_pitch,
199 yaw_angle: total_yaw,
200 pitch_rate: new_pitch_rate,
201 yaw_rate: new_yaw_rate,
202 precession_angle: new_precession_angle,
203 nutation_phase: new_nutation_phase,
204 }
205}
206
207pub fn calculate_epicyclic_motion(
209 spin_rate_rad_s: f64,
210 velocity_mps: f64,
211 stability_factor: f64,
212 time_s: f64,
213 initial_yaw_rad: f64,
214) -> (f64, f64) {
215 if stability_factor <= 1.0 {
216 return (initial_yaw_rad, initial_yaw_rad);
218 }
219
220 let omega_slow = 2.0 * velocity_mps / (stability_factor * spin_rate_rad_s);
223
224 let omega_fast = spin_rate_rad_s * ((stability_factor - 1.0).sqrt()) / stability_factor;
226
227 let amplitude_ratio = 1.0 / stability_factor;
229
230 let damping_factor = 0.1; let fast_amplitude = amplitude_ratio * (-damping_factor * omega_fast * time_s).exp();
233
234 let slow_phase = omega_slow * time_s;
236 let fast_phase = omega_fast * time_s;
237
238 let yaw = initial_yaw_rad * (slow_phase.cos() + fast_amplitude * fast_phase.cos());
240 let pitch = initial_yaw_rad * (slow_phase.sin() + fast_amplitude * fast_phase.sin());
241
242 (pitch, yaw)
243}
244
245pub fn calculate_limit_cycle_yaw(
247 velocity_mps: f64,
248 _spin_rate_rad_s: f64,
249 stability_factor: f64,
250 crosswind_mps: f64,
251) -> f64 {
252 let wind_yaw = if crosswind_mps != 0.0 && velocity_mps > 0.0 {
254 (crosswind_mps / velocity_mps).atan()
255 } else {
256 0.0
257 };
258
259 let yaw_of_repose = if stability_factor > 1.0 {
261 let repose_factor = 1.0 / (1.0 + 0.5 * (stability_factor - 1.0));
263 0.002 * repose_factor } else {
265 0.01 };
267
268 wind_yaw + yaw_of_repose
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274
275 #[test]
276 fn test_precession_frequency() {
277 let freq = calculate_precession_frequency(
278 17522.0, 850.0, 6.94e-8, 9.13e-7, 0.002, );
284
285 assert!(freq.abs() < 1.0);
287 }
288
289 #[test]
290 fn test_nutation_frequency() {
291 let freq = calculate_nutation_frequency(
292 17522.0, 6.94e-8, 9.13e-7, 1.5, );
297
298 assert!(freq > 1000.0);
300 assert!(freq < 10000.0);
301 }
302
303 #[test]
304 fn test_nutation_damping() {
305 let initial = 0.01;
306 let freq = 3000.0;
307
308 let amp_0 = calculate_nutation_amplitude(initial, 0.0, freq, 0.05, 17522.0);
310 let amp_1 = calculate_nutation_amplitude(initial, 0.1, freq, 0.05, 17522.0);
311
312 assert_eq!(amp_0, initial);
313 assert!(amp_1 < amp_0);
314 assert!(amp_1 > 0.0);
315 }
316
317 #[test]
318 fn test_precession_edge_cases() {
319 let freq_zero_vel = calculate_precession_frequency(
321 17522.0, 0.0, 6.94e-8, 9.13e-7, 0.002
322 );
323 assert_eq!(freq_zero_vel, 0.0);
324
325 let freq_zero_inertia = calculate_precession_frequency(
327 17522.0, 850.0, 6.94e-8, 0.0, 0.002
328 );
329 assert_eq!(freq_zero_inertia, 0.0);
330
331 let freq_zero_yaw = calculate_precession_frequency(
333 17522.0, 850.0, 6.94e-8, 9.13e-7, 0.0
334 );
335 assert_eq!(freq_zero_yaw, 0.0);
336 }
337
338 #[test]
339 fn test_nutation_edge_cases() {
340 let freq_unstable = calculate_nutation_frequency(
342 17522.0, 6.94e-8, 9.13e-7, 0.9
343 );
344 assert_eq!(freq_unstable, 0.0);
345
346 let freq_marginal = calculate_nutation_frequency(
348 17522.0, 6.94e-8, 9.13e-7, 1.0
349 );
350 assert_eq!(freq_marginal, 0.0);
351
352 let freq_zero_inertia = calculate_nutation_frequency(
354 17522.0, 6.94e-8, 0.0, 2.0
355 );
356 assert_eq!(freq_zero_inertia, 0.0);
357 }
358
359 #[test]
360 fn test_nutation_amplitude_bounds() {
361 let initial = 0.5; let freq = 3000.0;
363 let spin = 17522.0;
364
365 let amp = calculate_nutation_amplitude(initial, 0.0, freq, 0.05, spin);
367 assert!(amp <= 0.1); let amp_zero_freq = calculate_nutation_amplitude(initial, 1.0, 0.0, 0.05, spin);
371 assert_eq!(amp_zero_freq, 0.0);
372
373 let amp_zero_spin = calculate_nutation_amplitude(initial, 1.0, freq, 0.05, 0.0);
375 assert_eq!(amp_zero_spin, 0.0);
376 }
377
378 #[test]
379 fn test_epicyclic_motion() {
380 let (pitch, yaw) = calculate_epicyclic_motion(
381 17522.0, 850.0, 2.5, 0.1, 0.01 );
387
388 assert!(pitch.abs() <= 0.01);
390 assert!(yaw.abs() <= 0.01);
391
392 let (pitch_unstable, yaw_unstable) = calculate_epicyclic_motion(
394 17522.0, 850.0, 0.9, 0.1, 0.01
395 );
396 assert_eq!(pitch_unstable, 0.01);
397 assert_eq!(yaw_unstable, 0.01);
398 }
399
400 #[test]
401 fn test_limit_cycle_yaw() {
402 let yaw_wind = calculate_limit_cycle_yaw(
404 850.0, 17522.0, 2.5, 10.0 );
409
410 assert!(yaw_wind > 0.0);
412 assert!(yaw_wind < 0.1);
413
414 let yaw_no_wind = calculate_limit_cycle_yaw(
416 850.0, 17522.0, 2.5, 0.0
417 );
418 assert!(yaw_no_wind > 0.0);
419 assert!(yaw_no_wind < yaw_wind);
420
421 let yaw_unstable = calculate_limit_cycle_yaw(
423 850.0, 17522.0, 0.9, 0.0
424 );
425 assert_eq!(yaw_unstable, 0.01); }
427
428 #[test]
429 fn test_combined_angular_motion() {
430 let params = PrecessionNutationParams::default();
431 let initial_state = AngularState {
432 pitch_angle: 0.001,
433 yaw_angle: 0.002,
434 pitch_rate: 0.01,
435 yaw_rate: 0.01,
436 precession_angle: 0.0,
437 nutation_phase: 0.0,
438 };
439
440 let new_state = calculate_combined_angular_motion(
441 ¶ms,
442 &initial_state,
443 0.1, 0.001, 0.001 );
447
448 assert!(new_state.nutation_phase != initial_state.nutation_phase ||
451 new_state.precession_angle != initial_state.precession_angle);
452
453 assert!(new_state.pitch_angle.abs() < 1.0);
455 assert!(new_state.yaw_angle.abs() < 1.0);
456 }
457
458 #[test]
459 fn test_default_params() {
460 let params = PrecessionNutationParams::default();
461
462 assert!(params.mass_kg > 0.0);
464 assert!(params.caliber_m > 0.0);
465 assert!(params.length_m > 0.0);
466 assert!(params.spin_rate_rad_s > 0.0);
467 assert!(params.spin_inertia > 0.0);
468 assert!(params.transverse_inertia > 0.0);
469 assert!(params.velocity_mps > 0.0);
470 assert!(params.air_density_kg_m3 > 0.0);
471 assert!(params.mach > 0.0);
472 assert!(params.nutation_damping_factor > 0.0);
473 assert!(params.nutation_damping_factor < 1.0); }
475
476 #[test]
477 fn test_stability_effects() {
478 let freq_high_stability = calculate_nutation_frequency(
480 17522.0, 6.94e-8, 9.13e-7, 5.0
481 );
482
483 let freq_low_stability = calculate_nutation_frequency(
484 17522.0, 6.94e-8, 9.13e-7, 1.5
485 );
486
487 assert!(freq_high_stability > freq_low_stability);
489 }
490
491 #[test]
492 fn test_damping_time_evolution() {
493 let initial = 0.01;
494 let freq = 3000.0;
495 let spin = 17522.0;
496 let damping = 0.1;
497
498 let times = [0.0, 0.01, 0.02, 0.05, 0.1, 0.2];
500 let mut last_amp = initial;
501
502 for &t in ×[1..] {
503 let amp = calculate_nutation_amplitude(initial, t, freq, damping, spin);
504
505 assert!(amp < last_amp);
507 assert!(amp >= 0.0);
508 last_amp = amp;
509 }
510 }
511
512 #[test]
513 fn test_angular_state_evolution() {
514 let params = PrecessionNutationParams {
515 mass_kg: 0.01,
516 caliber_m: 0.008,
517 length_m: 0.03,
518 spin_rate_rad_s: 10000.0,
519 spin_inertia: 5e-8,
520 transverse_inertia: 8e-7,
521 velocity_mps: 800.0,
522 air_density_kg_m3: 1.2,
523 mach: 2.3,
524 pitch_damping_coeff: -0.5,
525 nutation_damping_factor: 0.08,
526 };
527
528 let mut state = AngularState {
529 pitch_angle: 0.0,
530 yaw_angle: 0.005,
531 pitch_rate: 0.0,
532 yaw_rate: 0.0,
533 precession_angle: 0.0,
534 nutation_phase: 0.0,
535 };
536
537 let initial_phase = state.nutation_phase;
539 let initial_precession = state.precession_angle;
540
541 let dt = 0.0001;
543 for i in 0..100 {
544 let time = i as f64 * dt;
545 state = calculate_combined_angular_motion(
546 ¶ms,
547 &state,
548 time,
549 dt,
550 0.002
551 );
552 }
553
554 assert!(state.precession_angle != initial_precession ||
556 state.nutation_phase != initial_phase);
557
558 assert!(state.yaw_angle.abs() < 0.1);
560 assert!(state.pitch_angle.abs() < 0.1);
561 }
562}