use crate::astro::orbit::{KeplerianOrbit, PreparedOrbit};
use crate::astro::units::GaussianYears;
use crate::calculus::conic_equations::{
elliptic_true_anomaly_and_radius, rotate_to_ecliptic, rotate_to_ecliptic_precomputed,
};
use crate::coordinates::cartesian::position::EclipticMeanJ2000;
use crate::qtty::*;
use crate::time::JulianDate;
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))
}
}
#[inline]
fn orbital_period_days(a: AstronomicalUnits) -> Days {
let a_val = a.value();
let t_gaussian_years = a_val * a_val.sqrt(); GaussianYears::new(t_gaussian_years).to::<Day>()
}
pub fn calculate_orbit_position(
elements: &KeplerianOrbit,
julian_date: JulianDate,
) -> EclipticMeanJ2000<AstronomicalUnit> {
let period = orbital_period_days(elements.shape().semi_major_axis());
type RadiansPerDay = crate::qtty::angular_rate::AngularRate<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 e = elements.shape().eccentricity();
let m_rad = (m0_rad + (n * dt).to::<Radian>()) % std::f64::consts::TAU;
let e_anomaly = solve_keplers_equation(m_rad, e);
let (true_anomaly, radius) =
elliptic_true_anomaly_and_radius(e_anomaly, e, elements.shape().semi_major_axis().value());
rotate_to_ecliptic(
radius,
elements.orientation().inclination(),
elements.orientation().argument_of_periapsis(),
elements.orientation().longitude_of_ascending_node(),
true_anomaly,
)
}
impl KeplerianOrbit {
pub fn kepler_position(&self, jd: JulianDate) -> EclipticMeanJ2000<AstronomicalUnit> {
calculate_orbit_position(self, jd)
}
}
pub fn calculate_prepared_position(
prepared: &PreparedOrbit,
julian_date: JulianDate,
) -> EclipticMeanJ2000<AstronomicalUnit> {
let dt = (julian_date - prepared.elements().epoch).value();
let m_rad = prepared.m0_rad() + prepared.mean_motion().value() * dt;
let m_rad = Radians::new(m_rad % std::f64::consts::TAU);
let e = prepared.elements().shape().eccentricity();
let e_anomaly = solve_keplers_equation(m_rad, e);
let (true_anomaly, radius) = elliptic_true_anomaly_and_radius(
e_anomaly,
e,
prepared.elements().shape().semi_major_axis().value(),
);
rotate_to_ecliptic_precomputed(radius, prepared.orientation_trig(), true_anomaly)
}
impl PreparedOrbit {
#[inline]
pub fn position_at(&self, jd: JulianDate) -> EclipticMeanJ2000<AstronomicalUnit> {
calculate_prepared_position(self, jd)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::macros::assert_cartesian_eq;
use crate::qtty::{Days, Degrees};
use crate::time::JulianDate;
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 = KeplerianOrbit::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 = KeplerianOrbit::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 = KeplerianOrbit::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 >= Radians::new(0.0));
assert!(computed_e <= Radians::new(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 >= Radians::new(0.0));
assert!(computed_e <= Radians::new(2.0 * PI));
let residual = kepler_equation_residual(computed_e, e, m);
assert!(residual.abs() < Radians::new(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_zero_semi_major_axis_fails_validation() {
assert!(KeplerianOrbit::try_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,
)
.is_err());
}
#[test]
fn test_calculate_orbit_position_different_epochs() {
let orbit = KeplerianOrbit::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.value() > 0.0);
}
}
#[test]
fn test_orbit_kepler_position_method() {
let orbit = KeplerianOrbit::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 = KeplerianOrbit::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().value() > 0.01);
let distance = coord.distance();
assert!(distance.value() > 0.0);
assert!(distance.value() < 2.0); }
#[test]
fn test_retrograde_orbit() {
let orbit = KeplerianOrbit::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());
}
}