use crate::constants::TAU;
use crate::secularlib::{A_NEPTUNE_AU, MASS_NEPTUNE_SOLAR};
pub fn resonance_semi_major_axis(p: u32, q: u32, a_planet: f64) -> f64 {
a_planet * (p as f64 / q as f64).powf(2.0 / 3.0)
}
pub fn resonant_angle(p: u32, q: u32, lambda: f64, lambda_planet: f64, varpi: f64) -> f64 {
let (pf, qf) = (p as f64, q as f64);
(pf * lambda - qf * lambda_planet - (pf - qf) * varpi).rem_euclid(TAU)
}
pub fn libration_amplitude(angles: &[f64], min_gap: f64) -> Option<f64> {
if angles.len() < 2 {
return None;
}
let mut sorted: Vec<f64> = angles.iter().map(|a| a.rem_euclid(TAU)).collect();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
let mut max_gap = TAU - (sorted[sorted.len() - 1] - sorted[0]);
for w in sorted.windows(2) {
max_gap = max_gap.max(w[1] - w[0]);
}
if max_gap > min_gap {
Some(0.5 * (TAU - max_gap))
} else {
None
}
}
pub fn is_librating(angles: &[f64], min_gap: f64) -> bool {
libration_amplitude(angles, min_gap).is_some()
}
pub fn chirikov_overlap_parameter(a_au: f64, q_au: f64, a_planet_au: f64, mass_ratio: f64) -> f64 {
let alpha = a_au / a_planet_au;
(24.0 / 5.0_f64.sqrt())
* alpha.powf(1.25)
* mass_ratio.sqrt()
* (-q_au * q_au / (2.0 * a_planet_au * a_planet_au)).exp()
}
pub fn chirikov_overlap_parameter_neptune(a_au: f64, q_au: f64) -> f64 {
chirikov_overlap_parameter(a_au, q_au, A_NEPTUNE_AU, MASS_NEPTUNE_SOLAR)
}
pub fn is_chaotic(a_au: f64, q_au: f64, a_planet_au: f64, mass_ratio: f64) -> bool {
chirikov_overlap_parameter(a_au, q_au, a_planet_au, mass_ratio) > 1.0
}
pub fn is_chaotic_neptune(a_au: f64, q_au: f64) -> bool {
is_chaotic(a_au, q_au, A_NEPTUNE_AU, MASS_NEPTUNE_SOLAR)
}
pub fn critical_perihelion(a_au: f64, a_planet_au: f64, mass_ratio: f64) -> f64 {
let alpha = a_au / a_planet_au;
let argument = (576.0 / 5.0) * mass_ratio * alpha.powf(2.5);
if argument <= 1.0 {
return 0.0;
}
a_planet_au * argument.ln().sqrt()
}
pub fn critical_perihelion_neptune(a_au: f64) -> f64 {
critical_perihelion(a_au, A_NEPTUNE_AU, MASS_NEPTUNE_SOLAR)
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn test_resonant_angle_stationary_at_exact_resonance() {
let (p, q) = (5u32, 2u32);
let n_planet = 1.0e-2; let n = n_planet * (q as f64) / (p as f64);
let varpi = 1.3;
let phi0 = resonant_angle(p, q, varpi, 0.0, varpi);
for step in 1..200 {
let t = step as f64 * 50.0;
let phi = resonant_angle(p, q, varpi + n * t, n_planet * t, varpi);
assert_relative_eq!(phi, phi0, epsilon = 1e-9);
}
}
#[test]
fn test_resonant_angle_circulates_off_resonance() {
let (p, q) = (3u32, 1u32);
let n_planet = 1.0e-2;
let n = n_planet * (q as f64) / (p as f64) * 1.05; let angles: Vec<f64> = (0..500)
.map(|s| {
let t = s as f64 * 200.0;
resonant_angle(p, q, n * t, n_planet * t, 0.0)
})
.collect();
assert!(!is_librating(&angles, 1.0));
}
#[test]
fn test_libration_detection() {
let librating: Vec<f64> = (0..200)
.map(|k| std::f64::consts::PI + 0.8 * (0.1 * k as f64).sin())
.collect();
let amp = libration_amplitude(&librating, 1.0).expect("should librate");
assert!((amp - 0.8).abs() < 0.05, "amplitude {amp}");
let circulating: Vec<f64> = (0..200).map(|k| k as f64 * TAU / 200.0).collect();
assert!(libration_amplitude(&circulating, 1.0).is_none());
}
#[test]
fn test_resonance_location() {
let a = resonance_semi_major_axis(3, 2, A_NEPTUNE_AU);
assert!((a - 39.4).abs() < 0.1, "a = {a}");
}
#[test]
fn test_chirikov_critical_perihelion_consistent_with_overlap() {
for &(a_pl, mu) in &[(A_NEPTUNE_AU, MASS_NEPTUNE_SOLAR), (19.19, 4.366_244_0e-5)] {
for &a in &[300.0, 500.0, 1000.0] {
let q_crit = critical_perihelion(a, a_pl, mu);
assert!(q_crit > 0.0);
let k = chirikov_overlap_parameter(a, q_crit, a_pl, mu);
assert_relative_eq!(k, 1.0, epsilon = 1e-10);
assert!(is_chaotic(a, q_crit - 1.0, a_pl, mu));
assert!(!is_chaotic(a, q_crit + 1.0, a_pl, mu));
}
}
}
#[test]
fn test_chirikov_trends() {
assert!(
chirikov_overlap_parameter_neptune(500.0, 35.0)
> chirikov_overlap_parameter_neptune(100.0, 35.0)
);
assert!(
chirikov_overlap_parameter_neptune(200.0, 30.0)
> chirikov_overlap_parameter_neptune(200.0, 45.0)
);
let q = critical_perihelion_neptune(500.0);
assert!(q > 25.0 && q < 60.0, "q_crit = {q}");
}
#[test]
fn test_neptune_critical_perihelion_pin() {
let q = critical_perihelion_neptune(500.0);
assert!((q - 41.4).abs() < 0.1, "q_crit(500) = {q}");
}
#[test]
fn test_neptune_wrappers_match_generic_form() {
for &(a, q) in &[(300.0, 35.0), (500.0, 41.0), (1000.0, 50.0)] {
assert_eq!(
chirikov_overlap_parameter_neptune(a, q),
chirikov_overlap_parameter(a, q, A_NEPTUNE_AU, MASS_NEPTUNE_SOLAR)
);
assert_eq!(
is_chaotic_neptune(a, q),
is_chaotic(a, q, A_NEPTUNE_AU, MASS_NEPTUNE_SOLAR)
);
}
assert_eq!(
critical_perihelion_neptune(500.0),
critical_perihelion(500.0, A_NEPTUNE_AU, MASS_NEPTUNE_SOLAR)
);
}
}