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 if params.transverse_inertia == 0.0 || params.velocity_mps == 0.0 || params.length_m == 0.0 {
135 return *angular_state;
137 }
138
139 let stability = (params.spin_inertia * params.spin_rate_rad_s.powi(2)) /
141 (4.0 * params.transverse_inertia * params.velocity_mps.powi(2) / params.length_m);
142
143 let omega_p = calculate_precession_frequency(
145 params.spin_rate_rad_s,
146 params.velocity_mps,
147 params.spin_inertia,
148 params.transverse_inertia,
149 angular_state.yaw_angle,
150 );
151
152 let omega_n = calculate_nutation_frequency(
154 params.spin_rate_rad_s,
155 params.spin_inertia,
156 params.transverse_inertia,
157 stability,
158 );
159
160 let nutation_amp = calculate_nutation_amplitude(
162 initial_disturbance,
163 time_s,
164 omega_n,
165 params.nutation_damping_factor,
166 params.spin_rate_rad_s,
167 );
168
169 let new_precession_angle = angular_state.precession_angle + omega_p * dt;
171
172 let new_nutation_phase = angular_state.nutation_phase + omega_n * dt;
174
175 let pitch_moment = calculate_pitch_damping_moment(
177 angular_state.pitch_rate,
178 params.velocity_mps,
179 params.air_density_kg_m3,
180 params.caliber_m,
181 params.length_m,
182 params.mach,
183 &PitchDampingCoefficients {
184 subsonic: params.pitch_damping_coeff,
185 ..Default::default()
186 },
187 );
188
189 let pitch_accel = pitch_moment / params.transverse_inertia;
191
192 let new_pitch_rate = angular_state.pitch_rate + pitch_accel * dt;
194 let new_yaw_rate = omega_p; let total_yaw = angular_state.yaw_angle + nutation_amp * new_nutation_phase.sin();
199
200 let new_pitch = angular_state.pitch_angle + new_pitch_rate * dt;
202
203 AngularState {
204 pitch_angle: new_pitch,
205 yaw_angle: total_yaw,
206 pitch_rate: new_pitch_rate,
207 yaw_rate: new_yaw_rate,
208 precession_angle: new_precession_angle,
209 nutation_phase: new_nutation_phase,
210 }
211}
212
213pub fn calculate_epicyclic_motion(
215 spin_rate_rad_s: f64,
216 velocity_mps: f64,
217 stability_factor: f64,
218 time_s: f64,
219 initial_yaw_rad: f64,
220) -> (f64, f64) {
221 if stability_factor <= 1.0 || spin_rate_rad_s == 0.0 {
223 return (initial_yaw_rad, initial_yaw_rad);
225 }
226
227 let omega_slow = 2.0 * velocity_mps / (stability_factor * spin_rate_rad_s);
230
231 let omega_fast = spin_rate_rad_s * ((stability_factor - 1.0).sqrt()) / stability_factor;
233
234 let amplitude_ratio = 1.0 / stability_factor;
236
237 let damping_factor = 0.1; let fast_amplitude = amplitude_ratio * (-damping_factor * omega_fast * time_s).exp();
240
241 let slow_phase = omega_slow * time_s;
243 let fast_phase = omega_fast * time_s;
244
245 let yaw = initial_yaw_rad * (slow_phase.cos() + fast_amplitude * fast_phase.cos());
247 let pitch = initial_yaw_rad * (slow_phase.sin() + fast_amplitude * fast_phase.sin());
248
249 (pitch, yaw)
250}
251
252pub fn calculate_limit_cycle_yaw(
254 velocity_mps: f64,
255 _spin_rate_rad_s: f64,
256 stability_factor: f64,
257 crosswind_mps: f64,
258) -> f64 {
259 let wind_yaw = if crosswind_mps != 0.0 && velocity_mps > 0.0 {
261 (crosswind_mps / velocity_mps).atan()
262 } else {
263 0.0
264 };
265
266 let yaw_of_repose = if stability_factor > 1.0 {
268 let repose_factor = 1.0 / (1.0 + 0.5 * (stability_factor - 1.0));
270 0.002 * repose_factor } else {
272 0.01 };
274
275 wind_yaw + yaw_of_repose
276}
277
278#[cfg(test)]
279mod tests {
280 use super::*;
281
282 #[test]
283 fn test_precession_frequency() {
284 let freq = calculate_precession_frequency(
285 17522.0, 850.0, 6.94e-8, 9.13e-7, 0.002, );
291
292 assert!(freq.abs() < 1.0);
294 }
295
296 #[test]
297 fn test_nutation_frequency() {
298 let freq = calculate_nutation_frequency(
299 17522.0, 6.94e-8, 9.13e-7, 1.5, );
304
305 assert!(freq > 1000.0);
307 assert!(freq < 10000.0);
308 }
309
310 #[test]
311 fn test_nutation_damping() {
312 let initial = 0.01;
313 let freq = 3000.0;
314
315 let amp_0 = calculate_nutation_amplitude(initial, 0.0, freq, 0.05, 17522.0);
317 let amp_1 = calculate_nutation_amplitude(initial, 0.1, freq, 0.05, 17522.0);
318
319 assert_eq!(amp_0, initial);
320 assert!(amp_1 < amp_0);
321 assert!(amp_1 > 0.0);
322 }
323
324 #[test]
325 fn test_precession_edge_cases() {
326 let freq_zero_vel = calculate_precession_frequency(
328 17522.0, 0.0, 6.94e-8, 9.13e-7, 0.002
329 );
330 assert_eq!(freq_zero_vel, 0.0);
331
332 let freq_zero_inertia = calculate_precession_frequency(
334 17522.0, 850.0, 6.94e-8, 0.0, 0.002
335 );
336 assert_eq!(freq_zero_inertia, 0.0);
337
338 let freq_zero_yaw = calculate_precession_frequency(
340 17522.0, 850.0, 6.94e-8, 9.13e-7, 0.0
341 );
342 assert_eq!(freq_zero_yaw, 0.0);
343 }
344
345 #[test]
346 fn test_nutation_edge_cases() {
347 let freq_unstable = calculate_nutation_frequency(
349 17522.0, 6.94e-8, 9.13e-7, 0.9
350 );
351 assert_eq!(freq_unstable, 0.0);
352
353 let freq_marginal = calculate_nutation_frequency(
355 17522.0, 6.94e-8, 9.13e-7, 1.0
356 );
357 assert_eq!(freq_marginal, 0.0);
358
359 let freq_zero_inertia = calculate_nutation_frequency(
361 17522.0, 6.94e-8, 0.0, 2.0
362 );
363 assert_eq!(freq_zero_inertia, 0.0);
364 }
365
366 #[test]
367 fn test_nutation_amplitude_bounds() {
368 let initial = 0.5; let freq = 3000.0;
370 let spin = 17522.0;
371
372 let amp = calculate_nutation_amplitude(initial, 0.0, freq, 0.05, spin);
374 assert!(amp <= 0.1); let amp_zero_freq = calculate_nutation_amplitude(initial, 1.0, 0.0, 0.05, spin);
378 assert_eq!(amp_zero_freq, 0.0);
379
380 let amp_zero_spin = calculate_nutation_amplitude(initial, 1.0, freq, 0.05, 0.0);
382 assert_eq!(amp_zero_spin, 0.0);
383 }
384
385 #[test]
386 fn test_epicyclic_motion() {
387 let (pitch, yaw) = calculate_epicyclic_motion(
388 17522.0, 850.0, 2.5, 0.1, 0.01 );
394
395 assert!(pitch.abs() <= 0.01);
397 assert!(yaw.abs() <= 0.01);
398
399 let (pitch_unstable, yaw_unstable) = calculate_epicyclic_motion(
401 17522.0, 850.0, 0.9, 0.1, 0.01
402 );
403 assert_eq!(pitch_unstable, 0.01);
404 assert_eq!(yaw_unstable, 0.01);
405 }
406
407 #[test]
408 fn test_limit_cycle_yaw() {
409 let yaw_wind = calculate_limit_cycle_yaw(
411 850.0, 17522.0, 2.5, 10.0 );
416
417 assert!(yaw_wind > 0.0);
419 assert!(yaw_wind < 0.1);
420
421 let yaw_no_wind = calculate_limit_cycle_yaw(
423 850.0, 17522.0, 2.5, 0.0
424 );
425 assert!(yaw_no_wind > 0.0);
426 assert!(yaw_no_wind < yaw_wind);
427
428 let yaw_unstable = calculate_limit_cycle_yaw(
430 850.0, 17522.0, 0.9, 0.0
431 );
432 assert_eq!(yaw_unstable, 0.01); }
434
435 #[test]
436 fn test_combined_angular_motion() {
437 let params = PrecessionNutationParams::default();
438 let initial_state = AngularState {
439 pitch_angle: 0.001,
440 yaw_angle: 0.002,
441 pitch_rate: 0.01,
442 yaw_rate: 0.01,
443 precession_angle: 0.0,
444 nutation_phase: 0.0,
445 };
446
447 let new_state = calculate_combined_angular_motion(
448 ¶ms,
449 &initial_state,
450 0.1, 0.001, 0.001 );
454
455 assert!(new_state.nutation_phase != initial_state.nutation_phase ||
458 new_state.precession_angle != initial_state.precession_angle);
459
460 assert!(new_state.pitch_angle.abs() < 1.0);
462 assert!(new_state.yaw_angle.abs() < 1.0);
463 }
464
465 #[test]
466 fn test_default_params() {
467 let params = PrecessionNutationParams::default();
468
469 assert!(params.mass_kg > 0.0);
471 assert!(params.caliber_m > 0.0);
472 assert!(params.length_m > 0.0);
473 assert!(params.spin_rate_rad_s > 0.0);
474 assert!(params.spin_inertia > 0.0);
475 assert!(params.transverse_inertia > 0.0);
476 assert!(params.velocity_mps > 0.0);
477 assert!(params.air_density_kg_m3 > 0.0);
478 assert!(params.mach > 0.0);
479 assert!(params.nutation_damping_factor > 0.0);
480 assert!(params.nutation_damping_factor < 1.0); }
482
483 #[test]
484 fn test_stability_effects() {
485 let freq_high_stability = calculate_nutation_frequency(
487 17522.0, 6.94e-8, 9.13e-7, 5.0
488 );
489
490 let freq_low_stability = calculate_nutation_frequency(
491 17522.0, 6.94e-8, 9.13e-7, 1.5
492 );
493
494 assert!(freq_high_stability > freq_low_stability);
496 }
497
498 #[test]
499 fn test_damping_time_evolution() {
500 let initial = 0.01;
501 let freq = 3000.0;
502 let spin = 17522.0;
503 let damping = 0.1;
504
505 let times = [0.0, 0.01, 0.02, 0.05, 0.1, 0.2];
507 let mut last_amp = initial;
508
509 for &t in ×[1..] {
510 let amp = calculate_nutation_amplitude(initial, t, freq, damping, spin);
511
512 assert!(amp < last_amp);
514 assert!(amp >= 0.0);
515 last_amp = amp;
516 }
517 }
518
519 #[test]
520 fn test_angular_state_evolution() {
521 let params = PrecessionNutationParams {
522 mass_kg: 0.01,
523 caliber_m: 0.008,
524 length_m: 0.03,
525 spin_rate_rad_s: 10000.0,
526 spin_inertia: 5e-8,
527 transverse_inertia: 8e-7,
528 velocity_mps: 800.0,
529 air_density_kg_m3: 1.2,
530 mach: 2.3,
531 pitch_damping_coeff: -0.5,
532 nutation_damping_factor: 0.08,
533 };
534
535 let mut state = AngularState {
536 pitch_angle: 0.0,
537 yaw_angle: 0.005,
538 pitch_rate: 0.0,
539 yaw_rate: 0.0,
540 precession_angle: 0.0,
541 nutation_phase: 0.0,
542 };
543
544 let initial_phase = state.nutation_phase;
546 let initial_precession = state.precession_angle;
547
548 let dt = 0.0001;
550 for i in 0..100 {
551 let time = i as f64 * dt;
552 state = calculate_combined_angular_motion(
553 ¶ms,
554 &state,
555 time,
556 dt,
557 0.002
558 );
559 }
560
561 assert!(state.precession_angle != initial_precession ||
563 state.nutation_phase != initial_phase);
564
565 assert!(state.yaw_angle.abs() < 0.1);
567 assert!(state.pitch_angle.abs() < 0.1);
568 }
569}