use std::sync::Arc;
use nalgebra::{Matrix3, Vector3};
use once_cell::sync::Lazy;
use crate::constants::{AU_KM, J2000};
use crate::framelib::{Frame, ECLIPJ2000_MATRIX_INV, GALACTIC};
use crate::time::Timescale;
use super::types::{MassiveBody, StateVector};
use super::{
GM_JUPITER, GM_SATURN, GM_SUN, GM_URANUS, MASS_JUPITER_SOLAR, MASS_SATURN_SOLAR,
MASS_URANUS_SOLAR,
};
#[inline]
pub fn perturbation_acceleration(
r: &Vector3<f64>,
r_pert: &Vector3<f64>,
gm_pert: f64,
) -> Vector3<f64> {
let dr = r - r_pert;
let dr_mag = dr.norm();
let r_pert_mag = r_pert.norm();
if dr_mag < 1e-30 || r_pert_mag < 1e-30 {
return Vector3::zeros();
}
let dr3 = dr_mag.powi(3);
let rp3 = r_pert_mag.powi(3);
-gm_pert * (dr / dr3 + r_pert / rp3)
}
#[inline]
pub fn direct_acceleration(r: &Vector3<f64>, r_pert: &Vector3<f64>, gm_pert: f64) -> Vector3<f64> {
let dr = r - r_pert;
let dr_mag = dr.norm();
if dr_mag < 1e-30 {
return Vector3::zeros();
}
-gm_pert * dr / dr_mag.powi(3)
}
pub fn kick_bodies_direct(bodies: &mut [MassiveBody], dt: f64) {
let n = bodies.len();
if n < 2 {
return;
}
let mut accels = vec![Vector3::<f64>::zeros(); n];
for i in 0..n {
for j in 0..n {
if i == j {
continue;
}
accels[i] +=
direct_acceleration(&bodies[i].state.pos, &bodies[j].state.pos, bodies[j].gm);
}
}
for (body, accel) in bodies.iter_mut().zip(accels.iter()) {
body.state.vel += dt * accel;
}
}
pub fn kick_particles_direct(
particles: &mut [StateVector],
active: &[bool],
bodies: &[MassiveBody],
dt: f64,
) {
for (i, particle) in particles.iter_mut().enumerate() {
if !active[i] {
continue;
}
let mut accel = Vector3::zeros();
for body in bodies {
accel += direct_acceleration(&particle.pos, &body.state.pos, body.gm);
}
particle.vel += dt * accel;
}
}
pub fn kick_bodies(bodies: &mut [MassiveBody], dt: f64) {
let n = bodies.len();
if n < 2 {
return;
}
let mut accels = vec![Vector3::<f64>::zeros(); n];
for i in 0..n {
for j in 0..n {
if i == j {
continue;
}
accels[i] +=
perturbation_acceleration(&bodies[i].state.pos, &bodies[j].state.pos, bodies[j].gm);
}
}
for (body, accel) in bodies.iter_mut().zip(accels.iter()) {
body.state.vel += dt * accel;
}
}
pub fn kick_particles(
particles: &mut [StateVector],
active: &[bool],
bodies: &[MassiveBody],
dt: f64,
) {
for (i, particle) in particles.iter_mut().enumerate() {
if !active[i] {
continue;
}
let mut accel = Vector3::zeros();
for body in bodies {
accel += perturbation_acceleration(&particle.pos, &body.state.pos, body.gm);
}
particle.vel += dt * accel;
}
}
pub fn j2_acceleration(
r: &Vector3<f64>,
gm: f64,
radius: f64,
j2: f64,
j4: Option<f64>,
) -> Vector3<f64> {
let r_mag = r.norm();
if r_mag < 1e-30 {
return Vector3::zeros();
}
let r2 = r_mag * r_mag;
let r5 = r2 * r2 * r_mag;
let r7 = r5 * r2;
let rr = radius * radius;
let z2 = r.z * r.z;
let z2_r2 = z2 / r2;
let factor_j2 = 1.5 * gm * j2 * rr;
let ax_j2 = -factor_j2 * r.x / r5 * (1.0 - 5.0 * z2_r2);
let ay_j2 = -factor_j2 * r.y / r5 * (1.0 - 5.0 * z2_r2);
let az_j2 = -factor_j2 * r.z / r5 * (3.0 - 5.0 * z2_r2);
let mut accel = Vector3::new(ax_j2, ay_j2, az_j2);
if let Some(j4_val) = j4 {
let r4 = rr * rr;
let r9 = r7 * r2;
let z4_r4 = z2_r2 * z2_r2;
let factor_j4 = 0.625 * gm * j4_val * r4;
let bracket = 3.0 - 42.0 * z2_r2 + 63.0 * z4_r4;
let bracket_z = 15.0 - 70.0 * z2_r2 + 63.0 * z4_r4;
accel.x += factor_j4 * r.x / r9 * bracket;
accel.y += factor_j4 * r.y / r9 * bracket;
accel.z += factor_j4 * r.z / r9 * bracket_z;
}
accel
}
pub fn effective_j2(mass_ratio: f64, a_planet_au: f64, r_ref_au: f64) -> f64 {
0.5 * mass_ratio * (a_planet_au / r_ref_au).powi(2)
}
pub fn combined_j2_giants() -> (f64, f64, f64) {
let r_ref = 1.0;
let j2_jup = effective_j2(MASS_JUPITER_SOLAR, 5.2026, r_ref);
let j2_sat = effective_j2(MASS_SATURN_SOLAR, 9.5549, r_ref);
let j2_ura = effective_j2(MASS_URANUS_SOLAR, 19.2184, r_ref);
let j2_total = j2_jup + j2_sat + j2_ura;
let j4_jup = -0.375 * MASS_JUPITER_SOLAR * (5.2026 / r_ref).powi(4);
let j4_sat = -0.375 * MASS_SATURN_SOLAR * (9.5549 / r_ref).powi(4);
let j4_ura = -0.375 * MASS_URANUS_SOLAR * (19.2184 / r_ref).powi(4);
let j4_total = j4_jup + j4_sat + j4_ura;
let gm_boost = GM_JUPITER + GM_SATURN + GM_URANUS;
(j2_total, j4_total, gm_boost)
}
pub fn secular_j2_acceleration(
pos: &Vector3<f64>,
gm_central: f64,
j2: f64,
j4: f64,
gm_boost: f64,
r_ref: f64,
) -> Vector3<f64> {
let r_mag = pos.norm();
if r_mag < 1e-30 {
return Vector3::zeros();
}
let gm_eff = gm_central + gm_boost;
let mut accel = j2_acceleration(pos, gm_eff, r_ref, j2, Some(j4));
accel -= gm_boost * pos / r_mag.powi(3);
accel
}
pub const RHO_MW_MSUN_AU3: f64 = 0.1 / {
let pc_km = 3.085_677_6e13_f64;
let ratio = pc_km / AU_KM;
ratio * ratio * ratio
};
static ECL_TO_GAL: Lazy<Matrix3<f64>> = Lazy::new(|| {
let eq_to_gal = GALACTIC.rotation_at(&Timescale::default().tt_jd(J2000, None));
eq_to_gal * *ECLIPJ2000_MATRIX_INV
});
pub fn galactic_pole_ecliptic() -> Vector3<f64> {
ECL_TO_GAL.row(2).transpose()
}
pub fn galactic_tide_acceleration(pos: &Vector3<f64>, rho_msun_au3: f64) -> Vector3<f64> {
let four_pi_g_rho = 4.0 * std::f64::consts::PI * GM_SUN * rho_msun_au3;
let pos_gal = *ECL_TO_GAL * pos;
let acc_gal = Vector3::new(0.0, 0.0, -four_pi_g_rho * pos_gal.z);
ECL_TO_GAL.transpose() * acc_gal
}
pub type CustomForce = Arc<dyn Fn(&Vector3<f64>) -> Vector3<f64> + Send + Sync>;
#[derive(Clone)]
pub enum ExtraForce {
J2Averaged {
j2: f64,
j4: f64,
gm_boost: f64,
r_ref_au: f64,
},
GalacticTide,
Custom(CustomForce),
}
impl ExtraForce {
pub fn j2_giants() -> Self {
let (j2, j4, gm_boost) = combined_j2_giants();
ExtraForce::J2Averaged {
j2,
j4,
gm_boost,
r_ref_au: 1.0,
}
}
pub fn acceleration(&self, pos: &Vector3<f64>) -> Vector3<f64> {
match self {
ExtraForce::J2Averaged {
j2,
j4,
gm_boost,
r_ref_au,
} => secular_j2_acceleration(pos, GM_SUN, *j2, *j4, *gm_boost, *r_ref_au),
ExtraForce::GalacticTide => galactic_tide_acceleration(pos, RHO_MW_MSUN_AU3),
ExtraForce::Custom(f) => f(pos),
}
}
}
impl std::fmt::Debug for ExtraForce {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ExtraForce::J2Averaged {
j2,
j4,
gm_boost,
r_ref_au,
} => write!(
f,
"ExtraForce::J2Averaged {{ j2: {j2:e}, j4: {j4:e}, gm_boost: {gm_boost:e}, \
r_ref_au: {r_ref_au} }}"
),
ExtraForce::GalacticTide => write!(f, "ExtraForce::GalacticTide"),
ExtraForce::Custom(_) => write!(f, "ExtraForce::Custom(..)"),
}
}
}
pub fn total_extra_acceleration(forces: &[ExtraForce], pos: &Vector3<f64>) -> Vector3<f64> {
let mut accel = Vector3::zeros();
for force in forces {
accel += force.acceleration(pos);
}
accel
}
#[cfg(test)]
mod tests {
use super::*;
use crate::constants::RAD2DEG;
use approx::assert_relative_eq;
#[test]
fn test_perturbation_self_consistency() {
let r = Vector3::new(5.0, 0.0, 0.0);
let r_pert = Vector3::new(-5.0, 0.0, 0.0);
let gm = 1e-4;
let accel = perturbation_acceleration(&r, &r_pert, gm);
assert!(accel.x > 0.0);
assert_relative_eq!(accel.x, gm * 0.03, epsilon = 1e-10);
}
#[test]
fn test_j2_equatorial_vs_polar() {
let gm = GM_SUN;
let r_eq = Vector3::new(1.0, 0.0, 0.0);
let r_pole = Vector3::new(0.0, 0.0, 1.0);
let a_eq = j2_acceleration(&r_eq, gm, 0.00465, 0.01, None);
let a_pole = j2_acceleration(&r_pole, gm, 0.00465, 0.01, None);
assert!(a_eq.x < 0.0);
assert!(a_pole.z > 0.0);
}
#[test]
fn test_gm_boost_magnitude() {
let (_, _, gm_boost) = combined_j2_giants();
let ratio = gm_boost / GM_SUN;
assert!(
(ratio - 1.28e-3).abs() < 5e-5,
"GM boost ratio {ratio:.3e} should be ~1.28e-3"
);
}
#[test]
fn test_secular_acceleration_dominated_by_monopole_at_large_r() {
let (j2, j4, gm_boost) = combined_j2_giants();
let pos = Vector3::new(500.0, 0.0, 0.0);
let a = secular_j2_acceleration(&pos, GM_SUN, j2, j4, gm_boost, 1.0);
let monopole = gm_boost / (500.0_f64 * 500.0);
assert!((a.norm() - monopole).abs() / monopole < 1e-3);
assert!(a.x < 0.0);
}
#[test]
fn test_galactic_pole_is_unit_and_inclined_60deg() {
let n = galactic_pole_ecliptic();
assert_relative_eq!(n.norm(), 1.0, epsilon = 1e-12);
let obliquity = n.z.acos() * RAD2DEG;
assert!((obliquity - 60.19).abs() < 0.1, "obliquity {obliquity}");
}
#[test]
fn test_tide_restoring_toward_galactic_plane() {
let n = galactic_pole_ecliptic();
let pos = 10_000.0 * n;
let a = galactic_tide_acceleration(&pos, RHO_MW_MSUN_AU3);
assert!(a.dot(&pos) < 0.0);
assert_relative_eq!(a.cross(&pos).norm(), 0.0, epsilon = 1e-20);
}
#[test]
fn test_tide_vanishes_in_galactic_plane() {
let n = galactic_pole_ecliptic();
let in_plane = n.cross(&Vector3::new(0.0, 0.0, 1.0)).normalize() * 10_000.0;
let a = galactic_tide_acceleration(&in_plane, RHO_MW_MSUN_AU3);
assert!(a.norm() < 1e-25);
}
#[test]
fn test_tide_matches_pole_projection_form() {
let four_pi_g_rho = 4.0 * std::f64::consts::PI * GM_SUN * RHO_MW_MSUN_AU3;
let n = galactic_pole_ecliptic();
let pos = Vector3::new(3_000.0, -7_000.0, 5_000.0);
let expected = -four_pi_g_rho * pos.dot(&n) * n;
let a = galactic_tide_acceleration(&pos, RHO_MW_MSUN_AU3);
assert_relative_eq!((a - expected).norm(), 0.0, epsilon = 1e-30);
}
#[test]
fn test_custom_force_composes() {
let drag = ExtraForce::Custom(Arc::new(|pos: &Vector3<f64>| -1e-12 * pos));
let forces = vec![drag, ExtraForce::GalacticTide];
let pos = Vector3::new(100.0, 0.0, 0.0);
let total = total_extra_acceleration(&forces, &pos);
let parts = forces[0].acceleration(&pos) + forces[1].acceleration(&pos);
assert_relative_eq!((total - parts).norm(), 0.0, epsilon = 1e-30);
}
}