use crate::consts;
use crate::mathtypes::*;
pub fn gr_schwarzschild_accel(pos_gcrf: &Vector3, vel_gcrf: &Vector3, mu_e: f64) -> Vector3 {
let r2 = pos_gcrf.norm_squared();
let r = r2.sqrt();
let v2 = vel_gcrf.norm_squared();
let rdotv = pos_gcrf.dot(vel_gcrf);
let c2 = consts::C * consts::C;
let factor = mu_e / (c2 * r2 * r);
let radial_coeff = 4.0 * mu_e / r - v2;
factor * (radial_coeff * pos_gcrf + 4.0 * rdotv * vel_gcrf)
}
pub const EARTH_ANGULAR_MOMENTUM_PER_MASS: f64 =
0.4 * consts::EARTH_RADIUS * consts::EARTH_RADIUS * consts::OMEGA_EARTH;
pub fn geodesic_precession_rate(sun_pos_gcrf: &Vector3, sun_vel_gcrf: &Vector3) -> Vector3 {
let r = sun_pos_gcrf.norm();
let c2 = consts::C * consts::C;
(1.5 * consts::MU_SUN / (c2 * r * r * r)) * sun_pos_gcrf.cross(sun_vel_gcrf)
}
pub fn gr_geodesic_accel(vel_gcrf: &Vector3, omega: &Vector3) -> Vector3 {
2.0 * omega.cross(vel_gcrf)
}
pub fn gr_lense_thirring_accel(
pos_gcrf: &Vector3,
vel_gcrf: &Vector3,
mu_e: f64,
j_gcrf: &Vector3,
) -> Vector3 {
let r2 = pos_gcrf.norm_squared();
let r = r2.sqrt();
let c2 = consts::C * consts::C;
let factor = 2.0 * mu_e / (c2 * r2 * r);
factor * ((3.0 / r2) * pos_gcrf.dot(j_gcrf) * pos_gcrf.cross(vel_gcrf) + vel_gcrf.cross(j_gcrf))
}
pub fn gr_accel(
pos_gcrf: &Vector3,
vel_gcrf: &Vector3,
mu_e: f64,
sun_pos_gcrf: &Vector3,
sun_vel_gcrf: &Vector3,
qitrf2gcrf: &Quaternion,
) -> Vector3 {
let omega = geodesic_precession_rate(sun_pos_gcrf, sun_vel_gcrf);
let j_gcrf = *qitrf2gcrf * numeris::vector![0.0, 0.0, EARTH_ANGULAR_MOMENTUM_PER_MASS];
gr_schwarzschild_accel(pos_gcrf, vel_gcrf, mu_e)
+ gr_geodesic_accel(vel_gcrf, &omega)
+ gr_lense_thirring_accel(pos_gcrf, vel_gcrf, mu_e, &j_gcrf)
}
#[cfg(test)]
mod tests {
use super::*;
fn sun_state() -> (Vector3, Vector3) {
(
numeris::vector![consts::AU, 0.0, 0.0],
numeris::vector![0.0, 29_780.0, 0.0],
)
}
#[test]
fn geodesic_rate_is_1_9_arcsec_per_century() {
let (sp, sv) = sun_state();
let omega = geodesic_precession_rate(&sp, &sv);
let expected = 1.92 * 4.848_137e-6 / (100.0 * 365.25 * 86400.0);
assert!(
(omega.norm() / expected - 1.0).abs() < 0.05,
"|Ω| = {:e} rad/s, expected ≈ {:e}",
omega.norm(),
expected
);
assert!(omega[0].abs() < 1e-30 && omega[1].abs() < 1e-30 && omega[2] > 0.0);
}
#[test]
fn geodesic_accel_magnitude_and_direction() {
let (sp, sv) = sun_state();
let omega = geodesic_precession_rate(&sp, &sv);
for r in [consts::EARTH_RADIUS + 400.0e3, 200_000.0e3] {
let v = (consts::MU_EARTH / r).sqrt();
let vel = numeris::vector![v, 0.0, 0.0]; let a = gr_geodesic_accel(&vel, &omega);
let expected = 2.0 * omega.norm() * v;
assert!((a.norm() / expected - 1.0).abs() < 1e-12);
assert!(a.dot(&vel).abs() < 1e-30, "geodesic term must be ⊥ v");
}
let v_leo = (consts::MU_EARTH / (consts::EARTH_RADIUS + 400.0e3)).sqrt();
let a_leo = gr_geodesic_accel(&numeris::vector![v_leo, 0.0, 0.0], &omega).norm();
assert!(
(1e-11..1e-10).contains(&a_leo),
"geodesic at LEO = {a_leo:e}"
);
}
#[test]
fn lense_thirring_magnitude_at_leo() {
let r = consts::EARTH_RADIUS + 400.0e3;
let v = (consts::MU_EARTH / r).sqrt();
let pos = numeris::vector![r, 0.0, 0.0];
let vel = numeris::vector![0.0, v, 0.0];
let j = numeris::vector![0.0, 0.0, EARTH_ANGULAR_MOMENTUM_PER_MASS];
let a = gr_lense_thirring_accel(&pos, &vel, consts::MU_EARTH, &j);
let c2 = consts::C * consts::C;
let expected =
2.0 * consts::MU_EARTH / (c2 * r * r * r) * v * EARTH_ANGULAR_MOMENTUM_PER_MASS;
assert!((a.norm() / expected - 1.0).abs() < 1e-12);
assert!(
(1e-11..1e-9).contains(&a.norm()),
"LT at LEO = {:e}",
a.norm()
);
assert!((EARTH_ANGULAR_MOMENTUM_PER_MASS / 1.186e9 - 1.0).abs() < 0.01);
}
#[test]
fn total_reduces_to_schwarzschild_without_sun_motion_and_spin() {
let r = consts::EARTH_RADIUS + 700.0e3;
let v = (consts::MU_EARTH / r).sqrt();
let pos = numeris::vector![r, 0.0, 0.0];
let vel = numeris::vector![0.0, v * 0.8, v * 0.6];
let omega =
geodesic_precession_rate(&numeris::vector![consts::AU, 0.0, 0.0], &Vector3::zeros());
assert_eq!(omega.norm(), 0.0);
let total = gr_schwarzschild_accel(&pos, &vel, consts::MU_EARTH)
+ gr_geodesic_accel(&vel, &omega)
+ gr_lense_thirring_accel(&pos, &vel, consts::MU_EARTH, &Vector3::zeros());
let s = gr_schwarzschild_accel(&pos, &vel, consts::MU_EARTH);
assert!((total - s).norm() < 1e-30);
}
#[test]
fn total_at_high_altitude_is_geodesic_dominated() {
let (sp, sv) = sun_state();
let r = 200_000.0e3;
let v = (consts::MU_EARTH / r).sqrt();
let pos = numeris::vector![r, 0.0, 0.0];
let vel = numeris::vector![0.0, v, 0.0];
let q = Quaternion::identity();
let total = gr_accel(&pos, &vel, consts::MU_EARTH, &sp, &sv, &q);
let s = gr_schwarzschild_accel(&pos, &vel, consts::MU_EARTH);
assert!(
total.norm() > 5.0 * s.norm(),
"total {:e} vs schwarzschild {:e}",
total.norm(),
s.norm()
);
}
#[test]
fn schwarzschild_at_geo_has_expected_magnitude() {
let r = consts::GEO_R;
let v = (consts::MU_EARTH / r).sqrt();
let pos = numeris::vector![r, 0.0, 0.0];
let vel = numeris::vector![0.0, v, 0.0];
let a = gr_schwarzschild_accel(&pos, &vel, consts::MU_EARTH);
let mag = a.norm();
assert!(
(1e-11..1e-8).contains(&mag),
"GR accel at GEO = {:e} m/s², expected ~few×1e-10",
mag
);
}
#[test]
fn schwarzschild_at_leo_has_expected_magnitude() {
let r = consts::EARTH_RADIUS + 500.0e3;
let v = (consts::MU_EARTH / r).sqrt();
let pos = numeris::vector![r, 0.0, 0.0];
let vel = numeris::vector![0.0, v, 0.0];
let a = gr_schwarzschild_accel(&pos, &vel, consts::MU_EARTH);
let mag = a.norm();
assert!(
(1e-10..1e-7).contains(&mag),
"GR accel at 500 km LEO = {:e} m/s², expected ~1e-9",
mag
);
}
#[test]
fn schwarzschild_points_outward_on_circular_orbit() {
let r = consts::EARTH_RADIUS + 1000.0e3;
let v = (consts::MU_EARTH / r).sqrt();
let pos = numeris::vector![r, 0.0, 0.0];
let vel = numeris::vector![0.0, v, 0.0];
let a = gr_schwarzschild_accel(&pos, &vel, consts::MU_EARTH);
assert!(a[1].abs() < 1e-20);
assert!(a[2].abs() < 1e-20);
assert!(a[0] > 0.0);
}
}