use std::f64::consts::PI;
fn coupling(a: f64, a_p: f64, e_p: f64, gm_p: f64) -> f64 {
let alpha = a / a_p;
gm_p * alpha * alpha / (4.0 * a_p * (1.0 - e_p * e_p).powf(1.5))
}
pub fn quadrupole_hamiltonian(
a: f64,
e: f64,
i: f64,
omega: f64,
a_p: f64,
e_p: f64,
gm_p: f64,
) -> f64 {
let c = coupling(a, a_p, e_p, gm_p);
let e2 = e * e;
let cos_i2 = i.cos().powi(2);
let sin_i2 = 1.0 - cos_i2;
-(c / 4.0)
* ((2.0 + 3.0 * e2) * (3.0 * cos_i2 - 1.0) + 15.0 * e2 * sin_i2 * (2.0 * omega).cos())
}
pub fn coplanar_quadrupole(a: f64, e: f64, a_p: f64, e_p: f64, gm_p: f64) -> f64 {
quadrupole_hamiltonian(a, e, 0.0, 0.0, a_p, e_p, gm_p)
}
fn orbit_position(a: f64, e: f64, i: f64, omega: f64, omega_big: f64, ea: f64) -> [f64; 3] {
let x_orb = a * (ea.cos() - e);
let y_orb = a * (1.0 - e * e).sqrt() * ea.sin();
let (sw, cw) = omega.sin_cos();
let (si, ci) = i.sin_cos();
let (so, co) = omega_big.sin_cos();
let x = (co * cw - so * sw * ci) * x_orb + (-co * sw - so * cw * ci) * y_orb;
let y = (so * cw + co * sw * ci) * x_orb + (-so * sw + co * cw * ci) * y_orb;
let z = sw * si * x_orb + cw * si * y_orb;
[x, y, z]
}
#[allow(clippy::too_many_arguments)]
pub fn numerical_secular_hamiltonian(
a: f64,
e: f64,
i: f64,
omega: f64,
omega_big: f64,
a_p: f64,
e_p: f64,
gm_p: f64,
n_quad: usize,
softening_au: f64,
) -> f64 {
let b2 = softening_au * softening_au;
let de = 2.0 * PI / n_quad as f64;
let mut p_pos = Vec::with_capacity(n_quad);
let mut p_weight = Vec::with_capacity(n_quad);
for kp in 0..n_quad {
let ea_p = (kp as f64 + 0.5) * de;
p_pos.push(orbit_position(a_p, e_p, 0.0, 0.0, 0.0, ea_p));
p_weight.push(1.0 - e_p * ea_p.cos());
}
let mut sum = 0.0;
for k in 0..n_quad {
let ea = (k as f64 + 0.5) * de;
let w = 1.0 - e * ea.cos();
let r = orbit_position(a, e, i, omega, omega_big, ea);
for (rp, wp) in p_pos.iter().zip(&p_weight) {
let dx = r[0] - rp[0];
let dy = r[1] - rp[1];
let dz = r[2] - rp[2];
let delta = (dx * dx + dy * dy + dz * dz + b2).sqrt();
sum += w * wp / delta;
}
}
-gm_p * sum * (de * de) / (4.0 * PI * PI)
}
pub fn phase_portrait(
a: f64,
a_p: f64,
e_p: f64,
gm_p: f64,
n_e: usize,
n_dvarpi: usize,
) -> (Vec<f64>, Vec<f64>, Vec<Vec<f64>>) {
let softening = 0.01 * a_p;
let n_quad = 64;
let e_vals: Vec<f64> = (0..n_e)
.map(|i| (i as f64 + 0.5) / n_e as f64 * 0.95)
.collect();
let dvarpi_vals: Vec<f64> = (0..n_dvarpi)
.map(|j| (j as f64) / n_dvarpi as f64 * 2.0 * PI - PI)
.collect();
let mut portrait = vec![vec![0.0; n_dvarpi]; n_e];
for (i, &e) in e_vals.iter().enumerate() {
for (j, &dv) in dvarpi_vals.iter().enumerate() {
portrait[i][j] = numerical_secular_hamiltonian(
a, e, 0.0, dv, 0.0, a_p, e_p, gm_p, n_quad, softening,
);
}
}
(e_vals, dvarpi_vals, portrait)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::secularlib::GM_SUN_AU3_DAY2;
const GM_P: f64 = 10.0 * 3.003489e-6 * GM_SUN_AU3_DAY2;
#[test]
fn test_quadrupole_has_no_apsidal_dependence() {
let h1 = coplanar_quadrupole(100.0, 0.5, 700.0, 0.6, GM_P);
let h2 = coplanar_quadrupole(100.0, 0.5, 700.0, 0.6, GM_P);
assert_eq!(h1, h2);
let ha = quadrupole_hamiltonian(100.0, 0.5, 0.0, 0.3, 700.0, 0.6, GM_P);
let hb = quadrupole_hamiltonian(100.0, 0.5, 0.0, 2.1, 700.0, 0.6, GM_P);
assert!((ha - hb).abs() < 1e-30);
}
#[test]
fn test_numerical_matches_quadrupole_at_small_alpha() {
let a = 30.0;
let a_p = 700.0;
let e_p = 0.0;
let n = 128;
let num =
|e: f64| numerical_secular_hamiltonian(a, e, 0.0, 0.0, 0.0, a_p, e_p, GM_P, n, 0.0);
let quad = |e: f64| coplanar_quadrupole(a, e, a_p, e_p, GM_P);
let d_num = num(0.6) - num(0.1);
let d_quad = quad(0.6) - quad(0.1);
let rel = ((d_num - d_quad) / d_quad).abs();
assert!(rel < 0.02, "numerical vs quadrupole ΔH mismatch: {rel:.3e}");
}
#[test]
fn test_numerical_axisymmetric_for_circular_perturber() {
let h0 =
numerical_secular_hamiltonian(300.0, 0.4, 0.0, 0.0, 0.0, 700.0, 0.0, GM_P, 96, 0.0);
let h1 = numerical_secular_hamiltonian(
300.0,
0.4,
0.0,
PI / 2.0,
0.0,
700.0,
0.0,
GM_P,
96,
0.0,
);
let rel = ((h0 - h1) / h0).abs();
assert!(
rel < 1e-10,
"circular perturber should be axisymmetric: {rel:.3e}"
);
}
#[test]
fn test_numerical_apsidal_structure_with_eccentric_perturber() {
let h_aligned =
numerical_secular_hamiltonian(250.0, 0.3, 0.0, 0.0, 0.0, 700.0, 0.6, GM_P, 128, 0.0);
let h_anti =
numerical_secular_hamiltonian(250.0, 0.3, 0.0, PI, 0.0, 700.0, 0.6, GM_P, 128, 0.0);
let rel = ((h_aligned - h_anti) / h_aligned).abs();
assert!(rel > 1e-6, "expected Δϖ asymmetry, got {rel:.3e}");
}
#[test]
fn test_numerical_quadrature_converged() {
let h64 =
numerical_secular_hamiltonian(200.0, 0.5, 0.2, 1.0, 0.4, 700.0, 0.5, GM_P, 64, 0.0);
let h128 =
numerical_secular_hamiltonian(200.0, 0.5, 0.2, 1.0, 0.4, 700.0, 0.5, GM_P, 128, 0.0);
let rel = ((h64 - h128) / h128).abs();
assert!(rel < 1e-8, "quadrature not converged: {rel:.3e}");
}
#[test]
fn test_phase_portrait_shape_and_finite() {
let (e_vals, dv_vals, portrait) = phase_portrait(400.0, 700.0, 0.6, GM_P, 8, 12);
assert_eq!(e_vals.len(), 8);
assert_eq!(dv_vals.len(), 12);
assert_eq!(portrait.len(), 8);
assert_eq!(portrait[0].len(), 12);
for row in &portrait {
for &h in row {
assert!(h.is_finite() && h < 0.0);
}
}
}
}