Skip to main content

ballistics_engine/
precession_nutation.rs

1//! Precession and Nutation Physics for Ballistic Projectiles
2//!
3//! This module implements the complex angular motion of spinning projectiles:
4//! - Precession: Slow coning motion of the projectile axis
5//! - Nutation: Fast oscillatory motion superimposed on precession
6//! - Angular momentum conservation
7//! - Gyroscopic effects
8
9// Precession and nutation modeling - now integrated!
10
11use crate::pitch_damping::{calculate_pitch_damping_moment, PitchDampingCoefficients};
12
13/// Complete angular state of the projectile
14#[derive(Debug, Clone, Copy)]
15pub struct AngularState {
16    pub pitch_angle: f64,      // Angle between axis and velocity (rad)
17    pub yaw_angle: f64,        // Angle in plane perpendicular to velocity (rad)
18    pub pitch_rate: f64,       // Rate of pitch angle change (rad/s)
19    pub yaw_rate: f64,         // Rate of yaw angle change (rad/s)
20    pub precession_angle: f64, // Cumulative precession angle (rad)
21    pub nutation_phase: f64,   // Phase of nutation oscillation (rad)
22}
23
24/// Parameters for precession and nutation calculations
25#[derive(Debug, Clone)]
26pub struct PrecessionNutationParams {
27    // Projectile properties
28    pub mass_kg: f64,
29    pub caliber_m: f64,
30    pub length_m: f64,
31    pub spin_rate_rad_s: f64,
32
33    // Moments of inertia
34    pub spin_inertia: f64,       // About longitudinal axis
35    pub transverse_inertia: f64, // About transverse axis
36
37    // Flight conditions
38    pub velocity_mps: f64,
39    pub air_density_kg_m3: f64,
40    pub mach: f64,
41
42    // Damping coefficients
43    pub pitch_damping_coeff: f64,
44    pub nutation_damping_factor: f64, // Fraction of critical damping
45}
46
47impl Default for PrecessionNutationParams {
48    fn default() -> Self {
49        Self {
50            mass_kg: 0.01134, // 175 grains
51            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
65/// The two epicyclic yaw-arm angular frequencies (rad/s) for a gyroscopically stable projectile:
66/// the FAST mode (nutation) and the SLOW mode (precession). Standard linearized aeroballistic
67/// result from the spinning-projectile yaw equation:
68///   phi_{fast,slow} = (Ix * p / 2 Iy) * [1 ± sqrt(1 - 1/Sg)]
69/// where Ix/Iy are the spin/transverse moments of inertia, p the spin rate, Sg the (dimensionless)
70/// gyroscopic stability factor. Returns (0, 0) when Sg <= 1 (no real epicyclic motion — the
71/// projectile is not gyroscopically stable) or the transverse inertia is zero. (MBA-941: the
72/// previous per-frequency formulas were dimensionally inconsistent — rad/m and length — and ad hoc.)
73pub 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    // Ix * p / 2 Iy  [rad/s] — the mean of the two arm rates.
83    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)) // (fast = nutation, slow = precession)
86}
87
88/// Slow-mode (precession) angular frequency in rad/s — the slow coning of the spin axis:
89/// phi_slow = (Ix p / 2 Iy)(1 - sqrt(1 - 1/Sg)).
90pub 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
99/// Fast-mode (nutation) angular frequency in rad/s:
100/// phi_fast = (Ix p / 2 Iy)(1 + sqrt(1 - 1/Sg)).
101pub 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
110/// Calculate nutation amplitude with exponential damping
111pub 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    // Damping rate
123    let damping_rate = damping_factor * nutation_frequency;
124
125    // Exponential decay
126    let amplitude = initial_disturbance_rad * (-damping_rate * time_s).exp();
127
128    // Clamp to reasonable bounds
129    amplitude.min(0.1) // Max 0.1 rad (~5.7 degrees)
130}
131
132/// Calculate the combined precession and nutation motion
133pub 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    // MBA-198: Guard against division by zero in stability calculation
141    if params.transverse_inertia == 0.0 || params.velocity_mps == 0.0 || params.length_m == 0.0 {
142        // Return unchanged state if invalid parameters
143        return *angular_state;
144    }
145
146    // Dimensionless gyroscopic stability factor via the Miller formula (consistent with the rest
147    // of the engine), derived from geometry. Twist (in/turn) follows from spin and velocity: one
148    // turn per 2*pi*V/p metres. (MBA-941: the previous inline Sg had units of 1/m.)
149    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    // Precession (slow) and nutation (fast) angular frequencies, both rad/s.
160    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    // Nutation amplitude (decaying)
174    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    // Update precession angle
183    let new_precession_angle = angular_state.precession_angle + omega_p * dt;
184
185    // Update nutation phase
186    let new_nutation_phase = angular_state.nutation_phase + omega_n * dt;
187
188    // Calculate pitch damping moment
189    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    // Angular acceleration from damping
203    let pitch_accel = pitch_moment / params.transverse_inertia;
204
205    // Update angular rates
206    let new_pitch_rate = angular_state.pitch_rate + pitch_accel * dt;
207
208    // MBA-941: bounded epicyclic yaw. Previously `total_yaw = yaw_angle + nutation_amp*sin(phase)`
209    // re-added the nutation to the carried-forward yaw every step, so the yaw random-walked and the
210    // precession rate (yaw_rate) was never actually integrated. The yaw is now a bounded function
211    // of the cumulative precession/nutation PHASES — a slow precession arm plus the damped fast
212    // nutation arm — and yaw_rate is its true time derivative.
213    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    // Pitch angle evolves more slowly
220    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
232/// Calculate the epicyclic (combined precession + nutation) motion
233pub 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    // MBA-198: Guard against division by zero
242    if stability_factor <= 1.0 || spin_rate_rad_s == 0.0 {
243        // Unstable or no spin - no regular motion
244        return (initial_yaw_rad, initial_yaw_rad);
245    }
246
247    // Fast (nutation) and slow (precession) angular frequencies, both rad/s (MBA-941).
248    let (omega_fast, omega_slow) =
249        epicyclic_frequencies(spin_inertia, transverse_inertia, spin_rate_rad_s, stability_factor);
250
251    // Amplitude ratio (fast/slow) — the nutation arm shrinks as stability grows.
252    let amplitude_ratio = 1.0 / stability_factor;
253
254    // Damping (exponential decay of fast mode)
255    let damping_factor = 0.1; // Typical value
256    let fast_amplitude = amplitude_ratio * (-damping_factor * omega_fast * time_s).exp();
257
258    // Combined motion
259    let slow_phase = omega_slow * time_s;
260    let fast_phase = omega_fast * time_s;
261
262    // Epicyclic coordinates
263    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
269/// Calculate the limit cycle yaw angle
270pub 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    // Base yaw from crosswind
277    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    // Yaw of repose (equilibrium)
284    let yaw_of_repose = if stability_factor > 1.0 {
285        // Typical value for spin-stabilized projectiles
286        let repose_factor = 1.0 / (1.0 + 0.5 * (stability_factor - 1.0));
287        0.002 * repose_factor // ~0.1 degrees nominal
288    } else {
289        0.01 // Larger for marginally stable
290    };
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        // Validate the corrected frequencies against the EXACT algebraic relations of the standard
302        // epicyclic decomposition (no external reference needed): with arm = Ix p / 2 Iy,
303        //   fast + slow = Ix p / Iy = 2*arm     (sum of the arm rates)
304        //   fast * slow = arm^2 / Sg            (product)
305        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        // Marginal stability (Sg -> 1+): the two modes coalesce at Ix p / 2 Iy.
320        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        // High stability: slow precession -> 0, fast nutation -> Ix p / Iy = 2*arm.
323        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        // Not gyroscopically stable -> no epicyclic motion.
327        assert_eq!(epicyclic_frequencies(ix, iy, p, 0.9), (0.0, 0.0));
328    }
329
330    #[test]
331    fn test_precession_frequency() {
332        // Slow (precession) mode, rad/s: (Ix p / 2 Iy)(1 - sqrt(1 - 1/Sg)).
333        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        // Positive, and slower than the nutation (fast) mode.
336        assert!(
337            freq > 0.0 && freq < nut,
338            "precession {freq} should satisfy 0 < freq < nutation {nut}"
339        );
340        // Unstable -> no precession.
341        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        // Fast (nutation) mode, rad/s: (Ix p / 2 Iy)(1 + sqrt(1 - 1/Sg)).
350        // arm = Ix p / 2 Iy ~= 666 rad/s; fast = arm*(1 + sqrt(1/3)) ~= 1050 rad/s.
351        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        // Check exponential decay
364        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        // Unstable / marginally stable -> no regular precession.
375        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        // Zero transverse inertia -> guarded to 0.
384        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        // Test unstable projectile (Sg <= 1)
393        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        // Test marginally stable (Sg = 1)
397        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        // Test zero transverse inertia
401        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; // Large initial disturbance
408        let freq = 3000.0;
409        let spin = 17522.0;
410
411        // Even with large initial disturbance, should be clamped
412        let amp = calculate_nutation_amplitude(initial, 0.0, freq, 0.05, spin);
413        assert!(amp <= 0.1); // Max 0.1 rad
414
415        // Test zero frequency
416        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        // Test zero spin
420        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, // spin inertia
428            9.13e-7, // transverse inertia
429            17522.0, // spin rate
430            2.5,     // stability factor
431            0.1,     // time
432            0.01,    // initial yaw
433        );
434
435        // Bounded by |initial_yaw| * (1 + 1/Sg) (slow arm amplitude 1 + fast arm amplitude 1/Sg).
436        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        // Unstable case -> returns the initial yaw unchanged.
441        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        // Test with crosswind
450        let yaw_wind = calculate_limit_cycle_yaw(
451            850.0,   // velocity
452            17522.0, // spin rate
453            2.5,     // stability
454            10.0,    // crosswind
455        );
456
457        // Should be small but non-zero
458        assert!(yaw_wind > 0.0);
459        assert!(yaw_wind < 0.1);
460
461        // Test without crosswind
462        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        // Test unstable projectile
467        let yaw_unstable = calculate_limit_cycle_yaw(850.0, 17522.0, 0.9, 0.0);
468        assert_eq!(yaw_unstable, 0.01); // Fixed value for unstable
469    }
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            &params,
485            &initial_state,
486            0.1,   // time
487            0.001, // dt
488            0.001, // initial disturbance
489        );
490
491        // Check that nutation phase evolved (it always should with non-zero frequency)
492        // Precession might be very small with small yaw angles
493        assert!(
494            new_state.nutation_phase != initial_state.nutation_phase
495                || new_state.precession_angle != initial_state.precession_angle
496        );
497
498        // Check reasonable bounds
499        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        // Check reasonable default values
508        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); // Should be fraction
519    }
520
521    #[test]
522    fn test_stability_effects() {
523        // Higher stability gives a higher nutation (fast-mode) frequency:
524        // phi_fast = (Ix p / 2 Iy)(1 + sqrt(1 - 1/Sg)) increases monotonically with Sg.
525        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        // Sample at different times
538        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 &times[1..] {
542            let amp = calculate_nutation_amplitude(initial, t, freq, damping, spin);
543
544            // Should monotonically decrease
545            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, // fast enough that the Miller Sg > 1 (gyroscopically stable)
558            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        // Store initial state for comparison
577        let initial_phase = state.nutation_phase;
578        let initial_precession = state.precession_angle;
579
580        // Evolve for several timesteps
581        let dt = 0.0001;
582        for i in 0..100 {
583            let time = i as f64 * dt;
584            state = calculate_combined_angular_motion(&params, &state, time, dt, 0.002);
585        }
586
587        // Should have evolved - at least one of these should change
588        assert!(
589            state.precession_angle != initial_precession || state.nutation_phase != initial_phase
590        );
591
592        // Should remain bounded
593        assert!(state.yaw_angle.abs() < 0.1);
594        assert!(state.pitch_angle.abs() < 0.1);
595    }
596}