use crate::math::{Fix128, Vec3Fix};
pub const KEPLER_MAX_ITERATIONS: u32 = 64;
pub const KEPLER_TOLERANCE: Fix128 = Fix128::from_raw(0, 1u64 << 20);
pub const DEGENERACY_TOLERANCE: Fix128 = Fix128::from_raw(0, 1u64 << 34);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum KeplerError {
NonPositiveGravitationalParameter,
NonPositiveSemiMajorAxis,
EccentricityOutOfRange,
InclinationOutOfRange,
NonPositiveRadius,
RadiusUnreachable,
ZeroPosition,
RadialTrajectory,
NotElliptic,
NonPositiveEquatorialRadius,
NotConverged,
}
impl core::fmt::Display for KeplerError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let msg = match self {
Self::NonPositiveGravitationalParameter => {
"the gravitational parameter must be positive"
}
Self::NonPositiveSemiMajorAxis => "the semi-major axis must be positive",
Self::EccentricityOutOfRange => {
"the eccentricity must be in [0, 1) (elliptic orbits only)"
}
Self::InclinationOutOfRange => "the inclination must be in [0, pi]",
Self::NonPositiveRadius => "the radius must be positive",
Self::RadiusUnreachable => {
"no elliptic orbit with this semi-major axis reaches the radius"
}
Self::ZeroPosition => "the position vector is zero",
Self::RadialTrajectory => "r x v = 0: a radial trajectory has no orbital plane",
Self::NotElliptic => "the specific energy is not negative: not an elliptic orbit",
Self::NonPositiveEquatorialRadius => "the equatorial radius must be positive",
Self::NotConverged => "Kepler's equation did not converge",
};
f.write_str(msg)
}
}
#[cfg(feature = "std")]
impl std::error::Error for KeplerError {}
fn check_mu(mu: Fix128) -> Result<(), KeplerError> {
if mu > Fix128::ZERO {
Ok(())
} else {
Err(KeplerError::NonPositiveGravitationalParameter)
}
}
fn check_a(a: Fix128) -> Result<(), KeplerError> {
if a > Fix128::ZERO {
Ok(())
} else {
Err(KeplerError::NonPositiveSemiMajorAxis)
}
}
fn check_e(e: Fix128) -> Result<(), KeplerError> {
if e.is_negative() || e >= Fix128::ONE {
Err(KeplerError::EccentricityOutOfRange)
} else {
Ok(())
}
}
fn wrap_two_pi(x: Fix128) -> Fix128 {
let k = (x / Fix128::TWO_PI).floor();
let mut r = x - Fix128::TWO_PI * k;
if r.is_negative() {
r = r + Fix128::TWO_PI;
}
if r >= Fix128::TWO_PI {
r = r - Fix128::TWO_PI;
}
r
}
fn turns_to_pi_range(x: Fix128) -> Fix128 {
((x + Fix128::PI) / Fix128::TWO_PI).floor()
}
pub fn solve_kepler(mean_anomaly: Fix128, eccentricity: Fix128) -> Result<Fix128, KeplerError> {
check_e(eccentricity)?;
let e = eccentricity;
let k = turns_to_pi_range(mean_anomaly);
let turns = Fix128::TWO_PI * k;
let m = mean_anomaly - turns;
let mut lo = m - e;
let mut hi = m + e;
let danby = Fix128::from_ratio(85, 100) * e;
let mut ecc = if m.sin().is_negative() {
m - danby
} else if m.is_zero() {
m
} else {
m + danby
};
for _ in 0..KEPLER_MAX_ITERATIONS {
let (s, c) = ecc.sin_cos();
let f = ecc - e * s - m;
if f.is_zero() {
return Ok(ecc + turns);
}
if f.is_negative() {
lo = ecc;
} else {
hi = ecc;
}
let fp = Fix128::ONE - e * c;
let mut next = ecc - f / fp;
if next <= lo || next >= hi {
next = (lo + hi).half();
}
let step = (next - ecc).abs();
ecc = next;
if step <= KEPLER_TOLERANCE || hi - lo <= KEPLER_TOLERANCE {
return Ok(ecc + turns);
}
}
Err(KeplerError::NotConverged)
}
pub fn true_from_eccentric_anomaly(
eccentric_anomaly: Fix128,
eccentricity: Fix128,
) -> Result<Fix128, KeplerError> {
check_e(eccentricity)?;
let (s, c) = eccentric_anomaly.half().sin_cos();
let y = (Fix128::ONE + eccentricity).sqrt() * s;
let x = (Fix128::ONE - eccentricity).sqrt() * c;
Ok(Fix128::atan2(y, x).double())
}
pub fn eccentric_from_true_anomaly(
true_anomaly: Fix128,
eccentricity: Fix128,
) -> Result<Fix128, KeplerError> {
check_e(eccentricity)?;
let (s, c) = true_anomaly.half().sin_cos();
let y = (Fix128::ONE - eccentricity).sqrt() * s;
let x = (Fix128::ONE + eccentricity).sqrt() * c;
Ok(Fix128::atan2(y, x).double())
}
pub fn mean_from_true_anomaly(
true_anomaly: Fix128,
eccentricity: Fix128,
) -> Result<Fix128, KeplerError> {
let ecc = eccentric_from_true_anomaly(true_anomaly, eccentricity)?;
Ok(wrap_two_pi(ecc - eccentricity * ecc.sin()))
}
pub fn true_from_mean_anomaly(
mean_anomaly: Fix128,
eccentricity: Fix128,
) -> Result<Fix128, KeplerError> {
let ecc = solve_kepler(wrap_two_pi(mean_anomaly), eccentricity)?;
Ok(wrap_two_pi(true_from_eccentric_anomaly(ecc, eccentricity)?))
}
pub fn mean_motion(mu: Fix128, semi_major_axis: Fix128) -> Result<Fix128, KeplerError> {
check_mu(mu)?;
check_a(semi_major_axis)?;
Ok((mu / semi_major_axis).sqrt() / semi_major_axis)
}
pub fn orbital_period(mu: Fix128, semi_major_axis: Fix128) -> Result<Fix128, KeplerError> {
check_mu(mu)?;
check_a(semi_major_axis)?;
Ok(Fix128::TWO_PI * semi_major_axis * (semi_major_axis / mu).sqrt())
}
pub fn vis_viva_speed(
mu: Fix128,
radius: Fix128,
semi_major_axis: Fix128,
) -> Result<Fix128, KeplerError> {
check_mu(mu)?;
check_a(semi_major_axis)?;
if radius <= Fix128::ZERO {
return Err(KeplerError::NonPositiveRadius);
}
let bracket = Fix128::from_int(2) / radius - Fix128::ONE / semi_major_axis;
if bracket <= Fix128::ZERO {
return Err(KeplerError::RadiusUnreachable);
}
Ok((mu * bracket).sqrt())
}
pub fn specific_orbital_energy(mu: Fix128, semi_major_axis: Fix128) -> Result<Fix128, KeplerError> {
check_mu(mu)?;
check_a(semi_major_axis)?;
Ok(-(mu / semi_major_axis.double()))
}
pub fn specific_angular_momentum(
mu: Fix128,
semi_major_axis: Fix128,
eccentricity: Fix128,
) -> Result<Fix128, KeplerError> {
check_mu(mu)?;
check_a(semi_major_axis)?;
check_e(eccentricity)?;
let p = semi_major_axis * (Fix128::ONE - eccentricity * eccentricity);
Ok(mu.sqrt() * p.sqrt())
}
pub fn j2_raan_rate(
mu: Fix128,
semi_major_axis: Fix128,
eccentricity: Fix128,
inclination: Fix128,
j2: Fix128,
equatorial_radius: Fix128,
) -> Result<Fix128, KeplerError> {
let base = j2_base(mu, semi_major_axis, eccentricity, j2, equatorial_radius)?;
let three_halves = Fix128::from_ratio(3, 2);
Ok(-(three_halves * base * inclination.cos()))
}
pub fn j2_arg_periapsis_rate(
mu: Fix128,
semi_major_axis: Fix128,
eccentricity: Fix128,
inclination: Fix128,
j2: Fix128,
equatorial_radius: Fix128,
) -> Result<Fix128, KeplerError> {
let base = j2_base(mu, semi_major_axis, eccentricity, j2, equatorial_radius)?;
let c = inclination.cos();
let three_quarters = Fix128::from_ratio(3, 4);
Ok(three_quarters * base * (Fix128::from_int(5) * c * c - Fix128::ONE))
}
fn j2_base(
mu: Fix128,
semi_major_axis: Fix128,
eccentricity: Fix128,
j2: Fix128,
equatorial_radius: Fix128,
) -> Result<Fix128, KeplerError> {
check_e(eccentricity)?;
let n = mean_motion(mu, semi_major_axis)?;
if equatorial_radius <= Fix128::ZERO {
return Err(KeplerError::NonPositiveEquatorialRadius);
}
let p = semi_major_axis * (Fix128::ONE - eccentricity * eccentricity);
let ratio = equatorial_radius / p;
Ok(n * j2 * ratio * ratio)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct StateVector {
pub position: Vec3Fix,
pub velocity: Vec3Fix,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct OrbitalElements {
pub semi_major_axis: Fix128,
pub eccentricity: Fix128,
pub inclination: Fix128,
pub raan: Fix128,
pub arg_periapsis: Fix128,
pub true_anomaly: Fix128,
}
impl OrbitalElements {
pub fn new(
semi_major_axis: Fix128,
eccentricity: Fix128,
inclination: Fix128,
raan: Fix128,
arg_periapsis: Fix128,
true_anomaly: Fix128,
) -> Result<Self, KeplerError> {
check_a(semi_major_axis)?;
check_e(eccentricity)?;
if inclination.is_negative() || inclination > Fix128::PI {
return Err(KeplerError::InclinationOutOfRange);
}
Ok(Self {
semi_major_axis,
eccentricity,
inclination,
raan: wrap_two_pi(raan),
arg_periapsis: wrap_two_pi(arg_periapsis),
true_anomaly: wrap_two_pi(true_anomaly),
})
}
fn validate(&self) -> Result<(), KeplerError> {
Self::new(
self.semi_major_axis,
self.eccentricity,
self.inclination,
self.raan,
self.arg_periapsis,
self.true_anomaly,
)
.map(|_| ())
}
#[must_use]
pub fn semi_latus_rectum(&self) -> Fix128 {
self.semi_major_axis * (Fix128::ONE - self.eccentricity * self.eccentricity)
}
pub fn mean_anomaly(&self) -> Result<Fix128, KeplerError> {
mean_from_true_anomaly(self.true_anomaly, self.eccentricity)
}
pub fn period(&self, mu: Fix128) -> Result<Fix128, KeplerError> {
orbital_period(mu, self.semi_major_axis)
}
pub fn to_state(&self, mu: Fix128) -> Result<StateVector, KeplerError> {
check_mu(mu)?;
self.validate()?;
let e = self.eccentricity;
let p = self.semi_latus_rectum();
let (sn, cn) = self.true_anomaly.sin_cos();
let radius = p / (Fix128::ONE + e * cn);
let speed = (mu / p).sqrt();
let r_pqw = (radius * cn, radius * sn);
let v_pqw = (-(speed * sn), speed * (e + cn));
let (so, co) = self.raan.sin_cos();
let (si, ci) = self.inclination.sin_cos();
let (sw, cw) = self.arg_periapsis.sin_cos();
let p_axis = Vec3Fix::new(co * cw - so * sw * ci, so * cw + co * sw * ci, sw * si);
let q_axis = Vec3Fix::new(-(co * sw) - so * cw * ci, co * cw * ci - so * sw, cw * si);
Ok(StateVector {
position: p_axis * r_pqw.0 + q_axis * r_pqw.1,
velocity: p_axis * v_pqw.0 + q_axis * v_pqw.1,
})
}
pub fn from_state(state: &StateVector, mu: Fix128) -> Result<Self, KeplerError> {
check_mu(mu)?;
let r_vec = state.position;
let v_vec = state.velocity;
let r = r_vec.length();
if r.is_zero() {
return Err(KeplerError::ZeroPosition);
}
let h_vec = r_vec.cross(v_vec);
let h = h_vec.length();
if h.is_zero() {
return Err(KeplerError::RadialTrajectory);
}
let v2 = v_vec.length_squared();
let mu_over_r = mu / r;
let energy = v2.half() - mu_over_r;
if !energy.is_negative() {
return Err(KeplerError::NotElliptic);
}
let a = -(mu / energy.double());
let e_vec = (r_vec * (v2 - mu_over_r) - v_vec * r_vec.dot(v_vec)) / mu;
let e = e_vec.length();
if e >= Fix128::ONE {
return Err(KeplerError::NotElliptic);
}
let h_hat = h_vec / h;
let n_vec = Vec3Fix::new(-h_vec.y, h_vec.x, Fix128::ZERO);
let n = n_vec.length();
let inclination = Fix128::atan2(n, h_vec.z);
let angle = |from: Vec3Fix, to: Vec3Fix| -> Fix128 {
wrap_two_pi(Fix128::atan2(h_hat.dot(from.cross(to)), from.dot(to)))
};
let circular = e < DEGENERACY_TOLERANCE;
let equatorial = n < DEGENERACY_TOLERANCE * h;
let (raan, arg_periapsis, true_anomaly) = match (circular, equatorial) {
(false, false) => (
wrap_two_pi(Fix128::atan2(n_vec.y, n_vec.x)),
angle(n_vec, e_vec),
angle(e_vec, r_vec),
),
(true, false) => (
wrap_two_pi(Fix128::atan2(n_vec.y, n_vec.x)),
Fix128::ZERO,
angle(n_vec, r_vec),
),
(false, true) => (
Fix128::ZERO,
angle(Vec3Fix::UNIT_X, e_vec),
angle(e_vec, r_vec),
),
(true, true) => (Fix128::ZERO, Fix128::ZERO, angle(Vec3Fix::UNIT_X, r_vec)),
};
Ok(Self {
semi_major_axis: a,
eccentricity: e,
inclination,
raan,
arg_periapsis,
true_anomaly,
})
}
pub fn propagate(&self, mu: Fix128, dt: Fix128) -> Result<Self, KeplerError> {
self.validate()?;
let n = mean_motion(mu, self.semi_major_axis)?;
let m0 = self.mean_anomaly()?;
let m1 = wrap_two_pi(m0 + n * dt);
Ok(Self {
true_anomaly: true_from_mean_anomaly(m1, self.eccentricity)?,
..*self
})
}
pub fn state_at(&self, mu: Fix128, dt: Fix128) -> Result<StateVector, KeplerError> {
self.propagate(mu, dt)?.to_state(mu)
}
}