use crate::pitch_damping::{calculate_pitch_damping_moment, PitchDampingCoefficients};
#[derive(Debug, Clone, Copy)]
pub struct AngularState {
pub pitch_angle: f64, pub yaw_angle: f64, pub pitch_rate: f64, pub yaw_rate: f64, pub precession_angle: f64, pub nutation_phase: f64, }
#[derive(Debug, Clone)]
pub struct PrecessionNutationParams {
pub mass_kg: f64,
pub caliber_m: f64,
pub length_m: f64,
pub spin_rate_rad_s: f64,
pub spin_inertia: f64, pub transverse_inertia: f64,
pub velocity_mps: f64,
pub air_density_kg_m3: f64,
pub mach: f64,
pub pitch_damping_coeff: f64,
pub nutation_damping_factor: f64, }
impl Default for PrecessionNutationParams {
fn default() -> Self {
Self {
mass_kg: 0.01134, caliber_m: 0.00782,
length_m: 0.033,
spin_rate_rad_s: 17522.0,
spin_inertia: 6.94e-8,
transverse_inertia: 9.13e-7,
velocity_mps: 850.0,
air_density_kg_m3: 1.225,
mach: 2.48,
pitch_damping_coeff: -0.8,
nutation_damping_factor: 0.05,
}
}
}
pub fn calculate_precession_frequency(
spin_rate_rad_s: f64,
velocity_mps: f64,
spin_inertia: f64,
transverse_inertia: f64,
yaw_angle_rad: f64,
) -> f64 {
if velocity_mps == 0.0 || transverse_inertia == 0.0 {
return 0.0;
}
(spin_inertia * spin_rate_rad_s * yaw_angle_rad.sin()) / (transverse_inertia * velocity_mps)
}
pub fn calculate_nutation_frequency(
spin_rate_rad_s: f64,
spin_inertia: f64,
transverse_inertia: f64,
stability_factor: f64,
) -> f64 {
if stability_factor <= 1.0 || transverse_inertia == 0.0 {
return 0.0;
}
let inertia_ratio = spin_inertia / transverse_inertia;
spin_rate_rad_s * inertia_ratio.sqrt() * (stability_factor - 1.0).sqrt()
}
pub fn calculate_nutation_amplitude(
initial_disturbance_rad: f64,
time_s: f64,
nutation_frequency: f64,
damping_factor: f64,
spin_rate_rad_s: f64,
) -> f64 {
if nutation_frequency == 0.0 || spin_rate_rad_s == 0.0 {
return 0.0;
}
let damping_rate = damping_factor * nutation_frequency;
let amplitude = initial_disturbance_rad * (-damping_rate * time_s).exp();
amplitude.min(0.1) }
pub fn calculate_combined_angular_motion(
params: &PrecessionNutationParams,
angular_state: &AngularState,
time_s: f64,
dt: f64,
initial_disturbance: f64,
) -> AngularState {
if params.transverse_inertia == 0.0 || params.velocity_mps == 0.0 || params.length_m == 0.0 {
return *angular_state;
}
let stability = (params.spin_inertia * params.spin_rate_rad_s.powi(2))
/ (4.0 * params.transverse_inertia * params.velocity_mps.powi(2) / params.length_m);
let omega_p = calculate_precession_frequency(
params.spin_rate_rad_s,
params.velocity_mps,
params.spin_inertia,
params.transverse_inertia,
angular_state.yaw_angle,
);
let omega_n = calculate_nutation_frequency(
params.spin_rate_rad_s,
params.spin_inertia,
params.transverse_inertia,
stability,
);
let nutation_amp = calculate_nutation_amplitude(
initial_disturbance,
time_s,
omega_n,
params.nutation_damping_factor,
params.spin_rate_rad_s,
);
let new_precession_angle = angular_state.precession_angle + omega_p * dt;
let new_nutation_phase = angular_state.nutation_phase + omega_n * dt;
let pitch_moment = calculate_pitch_damping_moment(
angular_state.pitch_rate,
params.velocity_mps,
params.air_density_kg_m3,
params.caliber_m,
params.length_m,
params.mach,
&PitchDampingCoefficients {
subsonic: params.pitch_damping_coeff,
..Default::default()
},
);
let pitch_accel = pitch_moment / params.transverse_inertia;
let new_pitch_rate = angular_state.pitch_rate + pitch_accel * dt;
let new_yaw_rate = omega_p;
let total_yaw = angular_state.yaw_angle + nutation_amp * new_nutation_phase.sin();
let new_pitch = angular_state.pitch_angle + new_pitch_rate * dt;
AngularState {
pitch_angle: new_pitch,
yaw_angle: total_yaw,
pitch_rate: new_pitch_rate,
yaw_rate: new_yaw_rate,
precession_angle: new_precession_angle,
nutation_phase: new_nutation_phase,
}
}
pub fn calculate_epicyclic_motion(
spin_rate_rad_s: f64,
velocity_mps: f64,
stability_factor: f64,
time_s: f64,
initial_yaw_rad: f64,
) -> (f64, f64) {
if stability_factor <= 1.0 || spin_rate_rad_s == 0.0 {
return (initial_yaw_rad, initial_yaw_rad);
}
let omega_slow = 2.0 * velocity_mps / (stability_factor * spin_rate_rad_s);
let omega_fast = spin_rate_rad_s * ((stability_factor - 1.0).sqrt()) / stability_factor;
let amplitude_ratio = 1.0 / stability_factor;
let damping_factor = 0.1; let fast_amplitude = amplitude_ratio * (-damping_factor * omega_fast * time_s).exp();
let slow_phase = omega_slow * time_s;
let fast_phase = omega_fast * time_s;
let yaw = initial_yaw_rad * (slow_phase.cos() + fast_amplitude * fast_phase.cos());
let pitch = initial_yaw_rad * (slow_phase.sin() + fast_amplitude * fast_phase.sin());
(pitch, yaw)
}
pub fn calculate_limit_cycle_yaw(
velocity_mps: f64,
_spin_rate_rad_s: f64,
stability_factor: f64,
crosswind_mps: f64,
) -> f64 {
let wind_yaw = if crosswind_mps != 0.0 && velocity_mps > 0.0 {
(crosswind_mps / velocity_mps).atan()
} else {
0.0
};
let yaw_of_repose = if stability_factor > 1.0 {
let repose_factor = 1.0 / (1.0 + 0.5 * (stability_factor - 1.0));
0.002 * repose_factor } else {
0.01 };
wind_yaw + yaw_of_repose
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_precession_frequency() {
let freq = calculate_precession_frequency(
17522.0, 850.0, 6.94e-8, 9.13e-7, 0.002, );
assert!(freq.abs() < 1.0);
}
#[test]
fn test_nutation_frequency() {
let freq = calculate_nutation_frequency(
17522.0, 6.94e-8, 9.13e-7, 1.5, );
assert!(freq > 1000.0);
assert!(freq < 10000.0);
}
#[test]
fn test_nutation_damping() {
let initial = 0.01;
let freq = 3000.0;
let amp_0 = calculate_nutation_amplitude(initial, 0.0, freq, 0.05, 17522.0);
let amp_1 = calculate_nutation_amplitude(initial, 0.1, freq, 0.05, 17522.0);
assert_eq!(amp_0, initial);
assert!(amp_1 < amp_0);
assert!(amp_1 > 0.0);
}
#[test]
fn test_precession_edge_cases() {
let freq_zero_vel = calculate_precession_frequency(17522.0, 0.0, 6.94e-8, 9.13e-7, 0.002);
assert_eq!(freq_zero_vel, 0.0);
let freq_zero_inertia = calculate_precession_frequency(17522.0, 850.0, 6.94e-8, 0.0, 0.002);
assert_eq!(freq_zero_inertia, 0.0);
let freq_zero_yaw = calculate_precession_frequency(17522.0, 850.0, 6.94e-8, 9.13e-7, 0.0);
assert_eq!(freq_zero_yaw, 0.0);
}
#[test]
fn test_nutation_edge_cases() {
let freq_unstable = calculate_nutation_frequency(17522.0, 6.94e-8, 9.13e-7, 0.9);
assert_eq!(freq_unstable, 0.0);
let freq_marginal = calculate_nutation_frequency(17522.0, 6.94e-8, 9.13e-7, 1.0);
assert_eq!(freq_marginal, 0.0);
let freq_zero_inertia = calculate_nutation_frequency(17522.0, 6.94e-8, 0.0, 2.0);
assert_eq!(freq_zero_inertia, 0.0);
}
#[test]
fn test_nutation_amplitude_bounds() {
let initial = 0.5; let freq = 3000.0;
let spin = 17522.0;
let amp = calculate_nutation_amplitude(initial, 0.0, freq, 0.05, spin);
assert!(amp <= 0.1);
let amp_zero_freq = calculate_nutation_amplitude(initial, 1.0, 0.0, 0.05, spin);
assert_eq!(amp_zero_freq, 0.0);
let amp_zero_spin = calculate_nutation_amplitude(initial, 1.0, freq, 0.05, 0.0);
assert_eq!(amp_zero_spin, 0.0);
}
#[test]
fn test_epicyclic_motion() {
let (pitch, yaw) = calculate_epicyclic_motion(
17522.0, 850.0, 2.5, 0.1, 0.01, );
assert!(pitch.abs() <= 0.01);
assert!(yaw.abs() <= 0.01);
let (pitch_unstable, yaw_unstable) =
calculate_epicyclic_motion(17522.0, 850.0, 0.9, 0.1, 0.01);
assert_eq!(pitch_unstable, 0.01);
assert_eq!(yaw_unstable, 0.01);
}
#[test]
fn test_limit_cycle_yaw() {
let yaw_wind = calculate_limit_cycle_yaw(
850.0, 17522.0, 2.5, 10.0, );
assert!(yaw_wind > 0.0);
assert!(yaw_wind < 0.1);
let yaw_no_wind = calculate_limit_cycle_yaw(850.0, 17522.0, 2.5, 0.0);
assert!(yaw_no_wind > 0.0);
assert!(yaw_no_wind < yaw_wind);
let yaw_unstable = calculate_limit_cycle_yaw(850.0, 17522.0, 0.9, 0.0);
assert_eq!(yaw_unstable, 0.01); }
#[test]
fn test_combined_angular_motion() {
let params = PrecessionNutationParams::default();
let initial_state = AngularState {
pitch_angle: 0.001,
yaw_angle: 0.002,
pitch_rate: 0.01,
yaw_rate: 0.01,
precession_angle: 0.0,
nutation_phase: 0.0,
};
let new_state = calculate_combined_angular_motion(
¶ms,
&initial_state,
0.1, 0.001, 0.001, );
assert!(
new_state.nutation_phase != initial_state.nutation_phase
|| new_state.precession_angle != initial_state.precession_angle
);
assert!(new_state.pitch_angle.abs() < 1.0);
assert!(new_state.yaw_angle.abs() < 1.0);
}
#[test]
fn test_default_params() {
let params = PrecessionNutationParams::default();
assert!(params.mass_kg > 0.0);
assert!(params.caliber_m > 0.0);
assert!(params.length_m > 0.0);
assert!(params.spin_rate_rad_s > 0.0);
assert!(params.spin_inertia > 0.0);
assert!(params.transverse_inertia > 0.0);
assert!(params.velocity_mps > 0.0);
assert!(params.air_density_kg_m3 > 0.0);
assert!(params.mach > 0.0);
assert!(params.nutation_damping_factor > 0.0);
assert!(params.nutation_damping_factor < 1.0); }
#[test]
fn test_stability_effects() {
let freq_high_stability = calculate_nutation_frequency(17522.0, 6.94e-8, 9.13e-7, 5.0);
let freq_low_stability = calculate_nutation_frequency(17522.0, 6.94e-8, 9.13e-7, 1.5);
assert!(freq_high_stability > freq_low_stability);
}
#[test]
fn test_damping_time_evolution() {
let initial = 0.01;
let freq = 3000.0;
let spin = 17522.0;
let damping = 0.1;
let times = [0.0, 0.01, 0.02, 0.05, 0.1, 0.2];
let mut last_amp = initial;
for &t in ×[1..] {
let amp = calculate_nutation_amplitude(initial, t, freq, damping, spin);
assert!(amp < last_amp);
assert!(amp >= 0.0);
last_amp = amp;
}
}
#[test]
fn test_angular_state_evolution() {
let params = PrecessionNutationParams {
mass_kg: 0.01,
caliber_m: 0.008,
length_m: 0.03,
spin_rate_rad_s: 10000.0,
spin_inertia: 5e-8,
transverse_inertia: 8e-7,
velocity_mps: 800.0,
air_density_kg_m3: 1.2,
mach: 2.3,
pitch_damping_coeff: -0.5,
nutation_damping_factor: 0.08,
};
let mut state = AngularState {
pitch_angle: 0.0,
yaw_angle: 0.005,
pitch_rate: 0.0,
yaw_rate: 0.0,
precession_angle: 0.0,
nutation_phase: 0.0,
};
let initial_phase = state.nutation_phase;
let initial_precession = state.precession_angle;
let dt = 0.0001;
for i in 0..100 {
let time = i as f64 * dt;
state = calculate_combined_angular_motion(¶ms, &state, time, dt, 0.002);
}
assert!(
state.precession_angle != initial_precession || state.nutation_phase != initial_phase
);
assert!(state.yaw_angle.abs() < 0.1);
assert!(state.pitch_angle.abs() < 0.1);
}
}