use crate::astro::orbit::Orbit;
use crate::coordinates::cartesian::position::EclipticMeanJ2000;
use crate::time::JulianDate;
use qtty::*;
use std::f64::consts::PI;
const TOLERANCE: Radians = Radians::new(1e-15);
const MAX_NEWTON_ITERS: usize = 100;
const MAX_BISECTION_ITERS: usize = 100;
#[inline]
fn kepler_equation_residual(e_anomaly: Radians, e: f64, m: Radians) -> Radians {
e_anomaly - Radians::new(e * e_anomaly.sin()) - m
}
#[inline]
fn kepler_equation_derivative(e_anomaly: Radians, e: f64) -> f64 {
1.0 - e * e_anomaly.cos()
}
fn newton_kepler(m: Radians, e: f64, initial_guess: Radians) -> Option<Radians> {
let mut e_anomaly = initial_guess;
for _ in 0..MAX_NEWTON_ITERS {
let f = kepler_equation_residual(e_anomaly, e, m);
let f_prime = kepler_equation_derivative(e_anomaly, e);
if f_prime.abs() < 1e-30 {
return None;
}
let delta = f / f_prime;
e_anomaly -= delta;
if delta.abs() < TOLERANCE {
return Some(e_anomaly); }
}
None }
fn bisection_kepler(m: Radians, e: f64, mut lower: Radians, mut upper: Radians) -> Radians {
let mut f_lower = kepler_equation_residual(lower, e, m);
let mut f_upper = kepler_equation_residual(upper, e, m);
while f_lower.signum() == f_upper.signum() {
upper += Radians::new(PI);
f_upper = kepler_equation_residual(upper, e, m);
}
for _ in 0..MAX_BISECTION_ITERS {
let mid = (lower + upper) * 0.5;
let f_mid = kepler_equation_residual(mid, e, m);
if f_mid.abs() < TOLERANCE {
return mid;
}
if f_lower.signum() == f_mid.signum() {
lower = mid;
f_lower = f_mid; } else {
upper = mid;
}
}
(lower + upper) * 0.5
}
pub fn solve_keplers_equation(m: Radians, e: f64) -> Radians {
let initial_guess = m;
if let Some(e_newton) = newton_kepler(m, e, initial_guess) {
e_newton
} else {
bisection_kepler(m, e, m - Radians::new(PI), m + Radians::new(PI))
}
}
fn orbital_period_days(a: AstronomicalUnits) -> Days {
let year = 365.256898326;
let t_years = a.value().powf(1.5); Days::new(t_years * year) }
pub fn calculate_orbit_position(
elements: &Orbit,
julian_date: JulianDate,
) -> EclipticMeanJ2000<AstronomicalUnit> {
if elements.semi_major_axis.value().abs() < 1e-30 {
return EclipticMeanJ2000::new(
AstronomicalUnits::new(0.0),
AstronomicalUnits::new(0.0),
AstronomicalUnits::new(0.0),
);
}
let period = orbital_period_days(elements.semi_major_axis);
type RadiansPerDay = qtty::frequency::Frequency<Radian, Day>;
let n: RadiansPerDay = Radians::TAU / period;
let dt: Days = julian_date - elements.epoch;
let m0_rad = elements.mean_anomaly_at_epoch.to::<Radian>();
let m_rad = (m0_rad + (n * dt).to::<Radian>()) % std::f64::consts::TAU;
let e_anomaly = solve_keplers_equation(m_rad, elements.eccentricity);
let e = elements.eccentricity;
let true_anomaly = 2.0 * ((1.0 + e).sqrt() * (e_anomaly * 0.5).tan() / (1.0 - e).sqrt()).atan();
let z = elements.semi_major_axis * (1.0 - e * e_anomaly.cos());
let i_rad = elements.inclination.to::<Radian>();
let omega_rad = elements.longitude_of_ascending_node.to::<Radian>();
let w_rad = elements.argument_of_perihelion.to::<Radian>();
let (sin_i, cos_i) = (i_rad.sin(), i_rad.cos());
let (sin_omega, cos_omega) = omega_rad.sin_cos();
let (sin_w_nu, cos_w_nu) = (w_rad + Radians::new(true_anomaly)).sin_cos();
EclipticMeanJ2000::new(
z * (cos_omega * cos_w_nu - sin_omega * sin_w_nu * cos_i),
z * (sin_omega * cos_w_nu + cos_omega * sin_w_nu * cos_i),
z * (sin_w_nu * sin_i),
)
}
impl Orbit {
pub fn kepler_position(&self, jd: JulianDate) -> EclipticMeanJ2000<AstronomicalUnit> {
calculate_orbit_position(self, jd)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::macros::assert_cartesian_eq;
use crate::time::JulianDate;
use qtty::{Days, Degrees};
fn approx_eq(a: f64, y: f64, tol: f64) -> bool {
(a - y).abs() < tol
}
#[test]
fn test_solve_keplers_equation() {
let e = 0.0;
let m = Radians::new(0.0);
let computed_e = solve_keplers_equation(m, e);
let expected_e = 0.0;
assert!((computed_e - Radians::new(expected_e)).abs() < Radians::new(1e-10));
let m = Radians::new(PI / 2.0);
let computed_e = solve_keplers_equation(m, e);
let expected_e = PI / 2.0;
assert!((computed_e - Radians::new(expected_e)).abs() < Radians::new(1e-10));
let e = 0.1;
let m = Radians::new(PI / 2.0);
let computed_e = solve_keplers_equation(m, e);
let expected_e = 1.670302; assert!((computed_e - Radians::new(expected_e)).abs() < Radians::new(1e-6));
}
#[test]
fn test_circular_orbit() {
let orbit = Orbit::new(
1.0 * AU, 0.0, Degrees::new(0.0), Degrees::new(0.0), Degrees::new(0.0), Degrees::new(0.0), JulianDate::J2000, );
let coord = calculate_orbit_position(&orbit, JulianDate::J2000);
assert_cartesian_eq!(coord, EclipticMeanJ2000::new(1.0, 0.0, 0.0), 1e-10);
let jd = JulianDate::J2000 + Days::new(90.0 / 0.9856076686); let coord = calculate_orbit_position(&orbit, jd);
assert_cartesian_eq!(coord, EclipticMeanJ2000::new(0.0, 1.0, 0.0), 1e-4);
}
#[test]
fn test_zero_inclination() {
let elements = Orbit::new(
2.0 * AU, 0.1, Degrees::new(0.0), Degrees::new(0.0), Degrees::new(0.0), Degrees::new(0.0), JulianDate::J2000, );
let coord = calculate_orbit_position(&elements, JulianDate::J2000);
assert_cartesian_eq!(coord, EclipticMeanJ2000::new(1.8, 0.0, 0.0), 1e-10);
}
#[test]
fn test_after_days() {
let elements = Orbit::new(
1.0 * AU, 0.0167, Degrees::new(0.0), Degrees::new(0.0), Degrees::new(0.0), Degrees::new(0.0), JulianDate::J2000, );
let coord = calculate_orbit_position(&elements, JulianDate::J2000 + Days::new(100.0));
let n = 0.9856076686; let m_deg = Degrees::new(0.0 + n * 100.0) % 360.0; let m_rad = m_deg.to::<Radian>();
let computed_r = coord.distance();
let expected_r = 1.0 * (1.0 - 0.0167 * m_rad.cos());
assert!(
(computed_r - AstronomicalUnits::new(expected_r)).abs() < AstronomicalUnits::new(1e-3)
); }
#[test]
fn test_planets_at_epoch() {
use crate::bodies::*;
struct PlanetTest<'a> {
name: &'a str,
planet: Planet,
expected: (f64, f64, f64),
tol: f64,
}
let planets = [
PlanetTest {
name: "Mercury",
planet: MERCURY,
expected: (-0.1302524, -0.4472397, -0.0245799),
tol: 1e-3,
},
PlanetTest {
name: "Venus",
planet: VENUS,
expected: (-0.7183069, -0.0325362, 0.0410162),
tol: 1e-3,
},
PlanetTest {
name: "Mars",
planet: MARS,
expected: (1.3907092, -0.0135668, -0.0344708),
tol: 1e-3,
},
PlanetTest {
name: "Jupiter",
planet: JUPITER,
expected: (4.0012926, 2.9384137, -0.1017857),
tol: 1e-2,
},
PlanetTest {
name: "Saturn",
planet: SATURN,
expected: (6.4066181, 6.5698012, -0.3690818),
tol: 5e-2,
},
PlanetTest {
name: "Uranus",
planet: URANUS,
expected: (14.4315748, -13.7346340, -0.2381392),
tol: 1e-2,
},
PlanetTest {
name: "Neptune",
planet: NEPTUNE,
expected: (16.8116521, -24.9919786, 0.1272357),
tol: 1e-2,
},
PlanetTest {
name: "Pluto",
planet: PLUTO,
expected: (-9.8758748, -27.9585114, 5.8505715),
tol: 1e-2,
},
];
for planet in &planets {
let coord = calculate_orbit_position(&planet.planet.orbit, JulianDate::J2000);
let expected =
EclipticMeanJ2000::new(planet.expected.0, planet.expected.1, planet.expected.2);
assert_cartesian_eq!(coord, expected, planet.tol, "{} at J2000", planet.name);
}
}
#[test]
fn test_kepler_equation_residual() {
let e_anomaly = Radians::new(PI / 2.0);
let e = 0.1;
let m = Radians::new(PI / 2.0);
let residual = kepler_equation_residual(e_anomaly, e, m);
assert!((residual - Radians::new(-0.1)).abs() < Radians::new(1e-10));
}
#[test]
fn test_kepler_equation_derivative() {
let e_anomaly = Radians::new(0.0);
let e = 0.1;
let derivative = kepler_equation_derivative(e_anomaly, e);
assert!(approx_eq(derivative, 0.9, 1e-10));
}
#[test]
fn test_solve_keplers_equation_edge_cases() {
let e = 1e-10;
let m = Radians::new(PI / 4.0);
let computed_e = solve_keplers_equation(m, e);
assert!((computed_e - m).abs() < Radians::new(1e-10));
let e = 0.99;
let m = Radians::new(PI / 2.0);
let computed_e = solve_keplers_equation(m, e);
assert!(computed_e.is_finite());
assert!(computed_e >= 0.0);
assert!(computed_e <= 2.0 * PI);
}
#[test]
fn test_solve_keplers_equation_full_range() {
let e = 0.1;
for i in 0..8 {
let m = Radians::new(i as f64 * PI / 4.0);
let computed_e = solve_keplers_equation(m, e);
assert!(computed_e.is_finite());
assert!(computed_e >= 0.0);
assert!(computed_e <= 2.0 * PI);
let residual = kepler_equation_residual(computed_e, e, m);
assert!(residual.abs() < 1e-10);
}
}
#[test]
fn test_orbital_period_days() {
let a = AstronomicalUnits::new(1.0); let period = orbital_period_days(a);
assert!((period - Days::new(365.256898326)).abs() < Days::new(1e-6));
let a = AstronomicalUnits::new(4.0); let period = orbital_period_days(a);
assert!((period - Days::new(2922.055186608)).abs() < Days::new(1e-6));
}
#[test]
fn test_calculate_orbit_position_edge_cases() {
let orbit = Orbit::new(
AstronomicalUnits::new(0.0), 0.0, Degrees::new(0.0), Degrees::new(0.0), Degrees::new(0.0), Degrees::new(0.0), JulianDate::J2000, );
let coord = calculate_orbit_position(&orbit, JulianDate::J2000);
assert!(coord.x().is_finite());
assert!(coord.y().is_finite());
assert!(coord.z().is_finite());
}
#[test]
fn test_calculate_orbit_position_different_epochs() {
let orbit = Orbit::new(
1.0 * AU, 0.1, Degrees::new(0.0), Degrees::new(0.0), Degrees::new(0.0), Degrees::new(0.0), JulianDate::J2000, );
let dates = [
JulianDate::J2000,
JulianDate::J2000 + Days::new(365.25),
JulianDate::J2000 + Days::new(730.5),
];
for &date in &dates {
let coord = calculate_orbit_position(&orbit, date);
assert!(coord.x().is_finite());
assert!(coord.y().is_finite());
assert!(coord.z().is_finite());
let distance = coord.distance();
assert!(distance > 0.0);
}
}
#[test]
fn test_orbit_kepler_position_method() {
let orbit = Orbit::new(
1.0 * AU, 0.0, Degrees::new(0.0), Degrees::new(0.0), Degrees::new(0.0), Degrees::new(0.0), JulianDate::J2000, );
let coord1 = orbit.kepler_position(JulianDate::J2000);
let coord2 = calculate_orbit_position(&orbit, JulianDate::J2000);
assert!((coord1.x() - coord2.x()).abs() < AstronomicalUnits::new(1e-10));
assert!((coord1.y() - coord2.y()).abs() < AstronomicalUnits::new(1e-10));
assert!((coord1.z() - coord2.z()).abs() < AstronomicalUnits::new(1e-10));
}
#[test]
fn test_high_inclination_orbit() {
let orbit = Orbit::new(
1.0 * AU, 0.1, Degrees::new(89.0), Degrees::new(0.0), Degrees::new(0.0), Degrees::new(90.0), JulianDate::J2000, );
let coord = calculate_orbit_position(&orbit, JulianDate::J2000);
assert!(coord.z().abs() > 0.01);
let distance = coord.distance();
assert!(distance > 0.0);
assert!(distance < 2.0); }
#[test]
fn test_retrograde_orbit() {
let orbit = Orbit::new(
1.0 * AU, 0.1, Degrees::new(180.0), Degrees::new(0.0), Degrees::new(0.0), Degrees::new(0.0), JulianDate::J2000, );
let coord = calculate_orbit_position(&orbit, JulianDate::J2000);
assert!(coord.x().is_finite());
assert!(coord.y().is_finite());
assert!(coord.z().is_finite());
}
}