use crate::{C, G, GravityParticle, GravitySolver};
use phyz_math::Vec3;
#[derive(Debug, Clone)]
pub struct PostNewtonianSolver {
pub max_order: f64,
pub softening: f64,
pub include_radiation: bool,
}
impl PostNewtonianSolver {
pub fn new(max_order: f64) -> Self {
Self {
max_order,
softening: 1e-3,
include_radiation: max_order >= 2.5,
}
}
fn newtonian_acceleration(
&self,
xi: Vec3,
_vi: Vec3,
_mi: f64,
xj: Vec3,
_vj: Vec3,
mj: f64,
) -> Vec3 {
let r = xj - xi;
let r2 = r.norm_squared() + self.softening * self.softening;
let r_mag = r2.sqrt();
G * mj / r2 * (r / r_mag)
}
fn pn1_acceleration(&self, xi: Vec3, vi: Vec3, mi: f64, xj: Vec3, vj: Vec3, mj: f64) -> Vec3 {
let r = xj - xi;
let r2 = r.norm_squared() + self.softening * self.softening;
let r_mag = r2.sqrt();
let n = r / r_mag;
let v2_i = vi.norm_squared();
let v2_j = vj.norm_squared();
let vi_dot_vj = vi.dot(&vj);
let vi_dot_n = vi.dot(&n);
let vj_dot_n = vj.dot(&n);
let c2 = C * C;
let schwarzschild = 4.0 * G * (mi + mj) / r_mag;
let coeff = G * mj / r2 / c2;
let term1 =
(schwarzschild - v2_i - 2.0 * v2_j + 4.0 * vi_dot_vj + 1.5 * vj_dot_n.powi(2)) * n;
let term2 = (4.0 * vi_dot_n - 3.0 * vj_dot_n) * vj;
coeff * (term1 + term2)
}
fn pn2_5_acceleration(&self, xi: Vec3, vi: Vec3, mi: f64, xj: Vec3, vj: Vec3, mj: f64) -> Vec3 {
if !self.include_radiation {
return Vec3::zeros();
}
let r = xj - xi;
let r2 = r.norm_squared() + self.softening * self.softening;
let r_mag = r2.sqrt();
let r3 = r_mag * r2;
let n = r / r_mag;
let v_rel = vi - vj;
let v_rel_dot_n = v_rel.dot(&n);
let c5 = C.powi(5);
let coeff = -8.0 / 5.0 * G * G * mi * mj * (mi + mj) / (c5 * r3);
coeff * (v_rel - 3.0 * v_rel_dot_n * n)
}
fn pn_acceleration(&self, xi: Vec3, vi: Vec3, mi: f64, xj: Vec3, vj: Vec3, mj: f64) -> Vec3 {
let mut a = self.newtonian_acceleration(xi, vi, mi, xj, vj, mj);
if self.max_order >= 1.0 {
a += self.pn1_acceleration(xi, vi, mi, xj, vj, mj);
}
if self.max_order >= 2.5 {
a += self.pn2_5_acceleration(xi, vi, mi, xj, vj, mj);
}
a
}
}
impl GravitySolver for PostNewtonianSolver {
fn compute_forces(&mut self, particles: &mut [GravityParticle]) {
let n = particles.len();
for p in particles.iter_mut() {
p.reset_force();
}
for i in 0..n {
for j in 0..n {
if i == j {
continue;
}
let xi = particles[i].x;
let vi = particles[i].v;
let mi = particles[i].m;
let xj = particles[j].x;
let vj = particles[j].v;
let mj = particles[j].m;
let a = self.pn_acceleration(xi, vi, mi, xj, vj, mj);
particles[i].add_force(a * mi);
}
}
}
fn potential_energy(&self, particles: &[GravityParticle]) -> f64 {
let n = particles.len();
let mut u = 0.0;
for i in 0..n {
for j in i + 1..n {
let r = (particles[j].x - particles[i].x).norm();
let r_soft = (r * r + self.softening * self.softening).sqrt();
u -= G * particles[i].m * particles[j].m / r_soft;
}
}
u
}
}
pub fn orbital_elements(x: Vec3, v: Vec3, m_central: f64) -> (f64, f64, f64, f64, f64, f64) {
let mu = G * m_central;
let r = x.norm();
let v2 = v.norm_squared();
let energy = v2 / 2.0 - mu / r;
let h = x.cross(&v);
let h_mag = h.norm();
let a = -mu / (2.0 * energy);
let e_vec = v.cross(&h) / mu - x / r;
let e = e_vec.norm();
let i = (h.z / h_mag).acos();
let n = Vec3::new(-h.y, h.x, 0.0);
let n_mag = n.norm();
let omega = if n_mag > 1e-10 {
let omega_raw = (n.x / n_mag).acos();
if n.y < 0.0 {
2.0 * std::f64::consts::PI - omega_raw
} else {
omega_raw
}
} else {
0.0
};
let omega_bar = if n_mag > 1e-10 && e > 1e-10 {
let omega_bar_raw = (n.dot(&e_vec) / (n_mag * e)).acos();
if e_vec.z < 0.0 {
2.0 * std::f64::consts::PI - omega_bar_raw
} else {
omega_bar_raw
}
} else {
0.0
};
let nu = if e > 1e-10 {
let nu_raw = (e_vec.dot(&x) / (e * r)).acos();
if x.dot(&v) < 0.0 {
2.0 * std::f64::consts::PI - nu_raw
} else {
nu_raw
}
} else {
0.0
};
(a, e, i, omega, omega_bar, nu)
}
pub fn perihelion_precession_rate(a: f64, e: f64, m_central: f64, period: f64) -> f64 {
let delta_omega_per_orbit =
6.0 * std::f64::consts::PI * G * m_central / (C * C * a * (1.0 - e * e));
let orbits_per_century = 3.15576e9 / period; let delta_omega_century = delta_omega_per_orbit * orbits_per_century;
delta_omega_century * 206265.0
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_newtonian_vs_pn() {
let mut solver_newton = PostNewtonianSolver::new(0.0); let mut solver_pn = PostNewtonianSolver::new(1.0);
let mut particles_newton = vec![
GravityParticle::new(Vec3::zeros(), Vec3::zeros(), 1e10),
GravityParticle::new(Vec3::new(1.0, 0.0, 0.0), Vec3::zeros(), 1e10),
];
let mut particles_pn = particles_newton.clone();
solver_newton.compute_forces(&mut particles_newton);
solver_pn.compute_forces(&mut particles_pn);
let diff = (particles_newton[0].f - particles_pn[0].f).norm();
assert!(diff < 1e-6);
}
#[test]
fn test_mercury_precession() {
let a = 57.9e9; let e = 0.2056; let m_sun = 1.989e30; let period = 87.969 * 24.0 * 3600.0;
let precession = perihelion_precession_rate(a, e, m_sun, period);
assert!(precession > 38.0 && precession < 48.0);
}
#[test]
fn test_orbital_elements() {
let m_sun = 1.989e30;
let r = 1.5e11; let v_circular = (G * m_sun / r).sqrt();
let x = Vec3::new(r, 0.0, 0.0);
let v = Vec3::new(0.0, v_circular, 0.0);
let (a, e, _i, _omega, _omega_bar, _nu) = orbital_elements(x, v, m_sun);
assert!((a - r).abs() / r < 1e-6);
assert!(e < 1e-6);
}
#[test]
fn test_2_5pn_radiation() {
let mut solver = PostNewtonianSolver::new(2.5);
let mut particles = vec![
GravityParticle::new(Vec3::new(-0.5e9, 0.0, 0.0), Vec3::new(0.0, 1e4, 0.0), 1e30),
GravityParticle::new(Vec3::new(0.5e9, 0.0, 0.0), Vec3::new(0.0, -1e4, 0.0), 1e30),
];
solver.compute_forces(&mut particles);
assert!(particles[0].f.norm() > 0.0);
}
}