apsis-rs/crates/orbit/apsis-kepler/src/anomaly.rs

126 lines
4.9 KiB
Rust

//! Conversions between the three anomalies.
//!
//! An anomaly is an angle that measures how far a satellite has travelled
//! around its orbit. The three forms used here are:
//!
//! - The true anomaly, the angle from perigee to the satellite as seen from
//! the focus where the central body sits. It is the only one with a direct
//! geometric meaning.
//! - The eccentric anomaly, the angle from perigee to the projection of the
//! satellite onto the auxiliary circle of the ellipse, as seen from the
//! center of the ellipse.
//! - The mean anomaly, a linear function of time that is proportional to the
//! area swept since perigee. It has no direct geometric meaning, but it is
//! the form in which an orbit propagates in time.
//!
//! Kepler's equation relates the mean and eccentric anomalies and has no
//! closed form solution. It is solved here by Newton's method from a
//! starting guess that is accurate enough to converge in a few iterations
//! for every eccentricity below one.
//!
//! All angles are in radians. The functions reduce their results into
//! `[-pi, pi)` so that a value is comparable without further wrapping.
use core::f64::consts::PI;
/// The maximum number of Newton iterations used to solve Kepler's equation.
const MAX_ITERATIONS: usize = 30;
/// The convergence tolerance of the Newton iteration, in radians.
const TOLERANCE: f64 = 1e-14;
/// Reduces an angle into `[-pi, pi)`.
pub fn wrap_to_pi(angle: f64) -> f64 {
let wrapped = (angle + PI).rem_euclid(2.0 * PI) - PI;
// `rem_euclid` can return exactly pi for an input just below pi, which
// the half-open interval excludes.
if wrapped >= PI {
wrapped - 2.0 * PI
} else {
wrapped
}
}
/// Returns the true anomaly for an eccentric anomaly and eccentricity.
///
/// The relation follows from the geometry of the auxiliary circle:
///
/// ```text
/// tan(nu / 2) = sqrt((1 + e) / (1 - e)) tan(E / 2)
/// ```
///
/// The half angle form is used rather than the direct cosine relation so
/// that the result is correct in all four quadrants without a quadrant
/// check.
pub fn true_from_eccentric(eccentric_anomaly: f64, eccentricity: f64) -> f64 {
let half = eccentric_anomaly / 2.0;
let factor = ((1.0 + eccentricity) / (1.0 - eccentricity)).sqrt();
wrap_to_pi(2.0 * (factor * half.tan()).atan())
}
/// Returns the eccentric anomaly for a true anomaly and eccentricity.
///
/// This is the inverse of [`true_from_eccentric`].
pub fn eccentric_from_true(true_anomaly: f64, eccentricity: f64) -> f64 {
let half = true_anomaly / 2.0;
let factor = ((1.0 - eccentricity) / (1.0 + eccentricity)).sqrt();
wrap_to_pi(2.0 * (factor * half.tan()).atan())
}
/// Returns the eccentric anomaly for a mean anomaly and eccentricity.
///
/// This solves Kepler's equation
///
/// ```text
/// M = E - e sin E
/// ```
///
/// by Newton's method. The starting guess is the mean anomaly plus a term
/// proportional to the eccentricity, which keeps the iteration stable for
/// eccentricities approaching one. The iteration always terminates, and the
/// result is the value reached after at most [`MAX_ITERATIONS`] steps.
pub fn eccentric_from_mean(mean_anomaly: f64, eccentricity: f64) -> f64 {
let mean = wrap_to_pi(mean_anomaly);
// A first order correction to the circular guess.
let mut eccentric = mean + eccentricity * mean.sin() * (1.0 + eccentricity * mean.cos());
for _ in 0..MAX_ITERATIONS {
let residual = eccentric - eccentricity * eccentric.sin() - mean;
let derivative = 1.0 - eccentricity * eccentric.cos();
if derivative.abs() < f64::MIN_POSITIVE {
break;
}
let step = residual / derivative;
eccentric -= step;
if step.abs() < TOLERANCE {
break;
}
}
wrap_to_pi(eccentric)
}
/// Returns the mean anomaly for an eccentric anomaly and eccentricity.
///
/// This is Kepler's equation evaluated directly, `M = E - e sin E`.
pub fn mean_from_eccentric(eccentric_anomaly: f64, eccentricity: f64) -> f64 {
wrap_to_pi(eccentric_anomaly - eccentricity * eccentric_anomaly.sin())
}
/// Returns the mean anomaly for a true anomaly and eccentricity.
///
/// The true anomaly is first converted to the eccentric anomaly, and the
/// mean anomaly then follows from Kepler's equation. Going through the
/// eccentric anomaly avoids the separate closed form for the swept area and
/// keeps a single expression responsible for the relation.
pub fn mean_from_true(true_anomaly: f64, eccentricity: f64) -> f64 {
let eccentric_anomaly = eccentric_from_true(true_anomaly, eccentricity);
mean_from_eccentric(eccentric_anomaly, eccentricity)
}
/// Returns the true anomaly for a mean anomaly and eccentricity.
pub fn true_from_mean(mean_anomaly: f64, eccentricity: f64) -> f64 {
true_from_eccentric(
eccentric_from_mean(mean_anomaly, eccentricity),
eccentricity,
)
}