use crate::constants::TAU;
use crate::secularlib::A_NEPTUNE_AU;
use std::f64::consts::PI;
const MAX_POINTS: usize = 1 << 18;
pub fn hansen_coefficient(j: i64, n: i32, m: i32, e: f64, tol: f64) -> f64 {
assert!((0.0..1.0).contains(&e), "eccentricity out of range: {e}");
let mut n_points = 64usize;
let mut prev = hansen_fixed(j, n, m, e, n_points);
while n_points < MAX_POINTS {
n_points *= 2;
let next = hansen_fixed(j, n, m, e, n_points);
if (next - prev).abs() <= tol * next.abs() + 1e-13 {
return next;
}
prev = next;
}
debug_assert!(
false,
"Hansen quadrature did not converge: j={j}, n={n}, m={m}, e={e}"
);
prev
}
fn hansen_fixed(j: i64, n: i32, m: i32, e: f64, n_points: usize) -> f64 {
let de = TAU / n_points as f64;
let mut sum = 0.0;
for k in 0..n_points {
let ea = (k as f64 + 0.5) * de;
let r_over_a = 1.0 - e * ea.cos();
let mean_anom = ea - e * ea.sin();
let true_anom = 2.0 * (((1.0 + e) / (1.0 - e)).sqrt() * (ea / 2.0).tan()).atan();
let integrand =
r_over_a.powi(n) * (m as f64 * true_anom - j as f64 * mean_anom).cos() * r_over_a;
sum += integrand;
}
sum * de / TAU
}
pub fn hansen_x0_neg3_0(e: f64) -> f64 {
(1.0 - e * e).powf(-1.5)
}
pub fn hansen_x_neg3_2_chain(j: i64, q_au: f64, a_planet_au: f64) -> f64 {
let jf = j.abs() as f64;
(2.0 * jf / 5.0) * (-(q_au / a_planet_au).powi(2)).exp()
}
pub fn hansen_x_neg3_2_neptune_chain(j: i64, q_au: f64) -> f64 {
hansen_x_neg3_2_chain(j, q_au, A_NEPTUNE_AU)
}
pub fn mean_to_true_anomaly(mean_anomaly: f64, e: f64) -> f64 {
let ea = solve_kepler_elliptic(e, mean_anomaly);
2.0 * (((1.0 + e) / (1.0 - e)).sqrt() * (ea / 2.0).tan()).atan()
}
fn solve_kepler_elliptic(e: f64, mean_anomaly: f64) -> f64 {
debug_assert!((0.0..1.0).contains(&e), "eccentricity out of range: {e}");
let mut m = mean_anomaly % TAU;
if m > PI {
m -= TAU;
} else if m < -PI {
m += TAU;
}
let sign = if m < 0.0 { -1.0 } else { 1.0 };
let m = m.abs();
let mut lo = m;
let mut hi = m + e;
let mut ea = (if e > 0.8 { PI } else { m + 0.85 * e }).clamp(lo, hi);
for _ in 0..100 {
let f = ea - e * ea.sin() - m;
if f > 0.0 {
hi = ea;
} else {
lo = ea;
}
let fp = 1.0 - e * ea.cos();
let step = f / fp;
if step.abs() < 1e-14 * ea.abs().max(1.0) {
ea -= step;
break;
}
let next = ea - step;
ea = if next > lo && next < hi {
next
} else {
0.5 * (lo + hi)
};
}
sign * ea
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn test_hansen_matches_closed_form_x0() {
for &e in &[0.0, 0.3, 0.7, 0.9] {
let num = hansen_coefficient(0, -3, 0, e, 1e-10);
let exact = hansen_x0_neg3_0(e);
assert_relative_eq!(num, exact, max_relative = 1e-8);
}
}
#[test]
fn test_hansen_x0_neg2_0_closed_form() {
for &e in &[0.0, 0.4, 0.8] {
let num = hansen_coefficient(0, -2, 0, e, 1e-10);
let exact = (1.0 - e * e).powf(-0.5);
assert_relative_eq!(num, exact, max_relative = 1e-8);
}
}
#[test]
fn test_hansen_x0_1_0_closed_form() {
let num = hansen_coefficient(0, 1, 0, 0.6, 1e-10);
assert_relative_eq!(num, 1.0 + 0.18, max_relative = 1e-8);
}
#[test]
fn test_hansen_circular_kronecker() {
assert_relative_eq!(
hansen_coefficient(2, -3, 2, 0.0, 1e-10),
1.0,
epsilon = 1e-10
);
assert!(hansen_coefficient(3, -3, 2, 0.0, 1e-10).abs() < 1e-10);
}
#[test]
fn test_hansen_high_e_converges() {
let v = hansen_coefficient(40, -3, 2, 0.95, 1e-8);
assert!(v.is_finite());
}
#[test]
fn test_neptune_chain_asymptotic_trends() {
let x = hansen_x_neg3_2_neptune_chain(20, 35.0);
assert!(x > 0.0);
assert!(hansen_x_neg3_2_neptune_chain(20, 30.0) > hansen_x_neg3_2_neptune_chain(20, 40.0));
assert!(hansen_x_neg3_2_neptune_chain(30, 35.0) > x);
}
#[test]
fn test_neptune_chain_matches_generic_form() {
for &(j, q) in &[(10, 30.0), (20, 35.0), (40, 50.0)] {
assert_eq!(
hansen_x_neg3_2_neptune_chain(j, q),
hansen_x_neg3_2_chain(j, q, A_NEPTUNE_AU)
);
}
}
#[test]
fn test_mean_to_true_anomaly() {
assert_relative_eq!(mean_to_true_anomaly(1.0, 0.0), 1.0, epsilon = 1e-10);
assert!(mean_to_true_anomaly(0.0, 0.7).abs() < 1e-10);
let f = mean_to_true_anomaly(0.5, 0.9);
assert!(f > 0.5 && f < PI);
}
#[test]
fn test_kepler_solver_residual() {
for &e in &[0.0, 0.1, 0.5, 0.9, 0.99] {
for k in 0..20 {
let m = -PI + (k as f64 + 0.5) * (TAU / 20.0);
let ea = solve_kepler_elliptic(e, m);
let residual = (ea - e * ea.sin() - m).abs();
assert!(residual < 1e-12, "e={e}, M={m}: residual {residual:.3e}");
}
}
}
}