use crate::anomalies::AnomalyError;
use crate::coords::Cartesian;
use crate::elements::keplerian::{GravitationalParameter, Keplerian, KeplerianError};
use crate::units::{AngleUnits, Distance};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Equinoctial {
a: Distance,
k: f64,
h: f64,
p: f64,
q: f64,
lambda: f64,
}
#[derive(Debug, Clone)]
pub enum EquinoctialError {
Anomaly(AnomalyError),
Keplerian(KeplerianError),
}
impl std::fmt::Display for EquinoctialError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Anomaly(e) => write!(f, "{e}"),
Self::Keplerian(e) => write!(f, "{e}"),
}
}
}
impl std::error::Error for EquinoctialError {}
impl From<AnomalyError> for EquinoctialError {
fn from(e: AnomalyError) -> Self {
Self::Anomaly(e)
}
}
impl From<KeplerianError> for EquinoctialError {
fn from(e: KeplerianError) -> Self {
Self::Keplerian(e)
}
}
impl Equinoctial {
pub fn new(a: Distance, k: f64, h: f64, p: f64, q: f64, lambda: f64) -> Self {
Self {
a,
k,
h,
p,
q,
lambda,
}
}
pub fn from_keplerian(kep: &Keplerian) -> Result<Self, AnomalyError> {
let m = kep.true_anomaly().to_mean(kep.eccentricity())?.as_f64();
let e = kep.eccentricity().as_f64();
let i = kep.inclination().as_f64();
let omega = kep.argument_of_periapsis().as_f64();
let big_omega = kep.longitude_of_ascending_node().as_f64();
let pomega = omega + big_omega; let k = e * pomega.cos();
let h = e * pomega.sin();
let half_i_tan = (i / 2.0).tan();
let p = half_i_tan * big_omega.cos();
let q = half_i_tan * big_omega.sin();
let lambda = m + pomega;
Ok(Self {
a: kep.semi_major_axis(),
k,
h,
p,
q,
lambda,
})
}
pub fn to_keplerian(&self) -> Result<Keplerian, EquinoctialError> {
let e = self.eccentricity();
let pomega = self.h.atan2(self.k); let half_i_tan = (self.p * self.p + self.q * self.q).sqrt();
let i = 2.0 * half_i_tan.atan();
let big_omega = self.q.atan2(self.p);
let omega = pomega - big_omega;
let m = self.lambda - pomega;
let kep = Keplerian::builder()
.with_semi_major_axis(self.a, e)
.with_inclination(i.rad())
.with_longitude_of_ascending_node(big_omega.rad().mod_two_pi())
.with_argument_of_periapsis(omega.rad().mod_two_pi())
.with_mean_anomaly(m.rad())
.build()?;
Ok(kep)
}
pub fn from_cartesian(
cart: &Cartesian,
mu: GravitationalParameter,
) -> Result<Self, AnomalyError> {
Self::from_keplerian(&cart.to_keplerian(mu))
}
pub fn to_cartesian(&self, mu: GravitationalParameter) -> Result<Cartesian, EquinoctialError> {
Ok(self.to_keplerian()?.to_cartesian(mu))
}
pub fn a(&self) -> Distance {
self.a
}
pub fn k(&self) -> f64 {
self.k
}
pub fn h(&self) -> f64 {
self.h
}
pub fn p(&self) -> f64 {
self.p
}
pub fn q(&self) -> f64 {
self.q
}
pub fn lambda(&self) -> f64 {
self.lambda
}
pub fn eccentricity(&self) -> f64 {
(self.k * self.k + self.h * self.h).sqrt()
}
pub fn inclination(&self) -> f64 {
2.0 * (self.p * self.p + self.q * self.q).sqrt().atan()
}
}
#[cfg(test)]
mod tests {
use lox_test_utils::assert_approx_eq;
use crate::elements::keplerian::GravitationalParameter;
use crate::units::{AngleUnits, DistanceUnits, VelocityUnits};
use super::*;
fn general_orbit_keplerian() -> Keplerian {
Keplerian::builder()
.with_semi_major_axis(24464.560.km(), 0.7311)
.with_inclination(0.122138.rad())
.with_longitude_of_ascending_node(1.00681.rad())
.with_argument_of_periapsis(3.10686.rad())
.with_true_anomaly(0.44369564302687126.rad())
.build()
.unwrap()
}
fn general_orbit_mu() -> GravitationalParameter {
GravitationalParameter::km3_per_s2(398600.43550702266)
}
#[test]
fn test_keplerian_roundtrip() {
let kep = general_orbit_keplerian();
let eq = Equinoctial::from_keplerian(&kep).unwrap();
let kep2 = eq.to_keplerian().unwrap();
assert_approx_eq!(
kep.semi_major_axis().as_f64(),
kep2.semi_major_axis().as_f64(),
rtol <= 1e-12
);
assert_approx_eq!(
kep.eccentricity().as_f64(),
kep2.eccentricity().as_f64(),
atol <= 1e-12
);
assert_approx_eq!(
kep.inclination().as_f64(),
kep2.inclination().as_f64(),
atol <= 1e-12
);
let mu = general_orbit_mu();
let c1 = kep.to_cartesian(mu);
let c2 = kep2.to_cartesian(mu);
assert_approx_eq!(c1.position(), c2.position(), rtol <= 1e-10);
assert_approx_eq!(c1.velocity(), c2.velocity(), rtol <= 1e-10);
}
#[test]
fn test_cartesian_roundtrip() {
let mu = general_orbit_mu();
let cart = Cartesian::builder()
.position(
-0.107622532467967e7.m(),
-0.676589636432773e7.m(),
-0.332308783350379e6.m(),
)
.velocity(
0.935685775154103e4.mps(),
-0.331234775037644e4.mps(),
-0.118801577532701e4.mps(),
)
.build();
let eq = Equinoctial::from_cartesian(&cart, mu).unwrap();
let cart2 = eq.to_cartesian(mu).unwrap();
assert_approx_eq!(cart.position(), cart2.position(), rtol <= 1e-8);
assert_approx_eq!(cart.velocity(), cart2.velocity(), rtol <= 1e-6);
}
#[test]
fn test_circular_orbit() {
let kep = Keplerian::builder()
.with_semi_major_axis(6878.137.km(), 0.0)
.with_inclination(97.42_f64.to_radians().rad())
.with_longitude_of_ascending_node(69.3_f64.to_radians().rad())
.with_argument_of_periapsis(0.0.rad())
.with_true_anomaly(45.0_f64.to_radians().rad())
.build()
.unwrap();
let eq = Equinoctial::from_keplerian(&kep).unwrap();
assert_approx_eq!(eq.k(), 0.0, atol <= 1e-15);
assert_approx_eq!(eq.h(), 0.0, atol <= 1e-15);
assert_approx_eq!(eq.eccentricity(), 0.0, atol <= 1e-15);
assert_approx_eq!(eq.inclination(), 97.42_f64.to_radians(), atol <= 1e-12);
assert!(eq.p().abs() > 0.1);
assert!(eq.q().abs() > 0.1);
let mu = GravitationalParameter::km3_per_s2(398600.4418);
let c1 = kep.to_cartesian(mu);
let c2 = eq.to_cartesian(mu).unwrap();
assert_approx_eq!(c1.position(), c2.position(), rtol <= 1e-8);
assert_approx_eq!(c1.velocity(), c2.velocity(), rtol <= 1e-6);
}
#[test]
fn test_equatorial_orbit() {
let kep = Keplerian::builder()
.with_semi_major_axis(42164.0.km(), 0.001)
.with_inclination(0.0.rad())
.with_longitude_of_ascending_node(0.0.rad())
.with_argument_of_periapsis(45.0_f64.to_radians().rad())
.with_true_anomaly(30.0_f64.to_radians().rad())
.build()
.unwrap();
let eq = Equinoctial::from_keplerian(&kep).unwrap();
assert_approx_eq!(eq.p(), 0.0, atol <= 1e-15);
assert_approx_eq!(eq.q(), 0.0, atol <= 1e-15);
assert_approx_eq!(eq.inclination(), 0.0, atol <= 1e-15);
assert!(eq.eccentricity() > 0.0);
let mu = GravitationalParameter::km3_per_s2(398600.4418);
let c1 = kep.to_cartesian(mu);
let c2 = eq.to_cartesian(mu).unwrap();
assert_approx_eq!(c1.position(), c2.position(), rtol <= 1e-8);
assert_approx_eq!(c1.velocity(), c2.velocity(), rtol <= 1e-6);
}
#[test]
fn test_circular_equatorial_orbit() {
let kep = Keplerian::builder()
.with_semi_major_axis(42164.0.km(), 0.0)
.with_inclination(0.0.rad())
.with_longitude_of_ascending_node(0.0.rad())
.with_argument_of_periapsis(0.0.rad())
.with_true_anomaly(90.0_f64.to_radians().rad())
.build()
.unwrap();
let eq = Equinoctial::from_keplerian(&kep).unwrap();
assert_approx_eq!(eq.k(), 0.0, atol <= 1e-15);
assert_approx_eq!(eq.h(), 0.0, atol <= 1e-15);
assert_approx_eq!(eq.p(), 0.0, atol <= 1e-15);
assert_approx_eq!(eq.q(), 0.0, atol <= 1e-15);
assert_approx_eq!(eq.eccentricity(), 0.0, atol <= 1e-15);
assert_approx_eq!(eq.inclination(), 0.0, atol <= 1e-15);
assert_approx_eq!(eq.lambda(), 90.0_f64.to_radians(), atol <= 1e-12);
let mu = GravitationalParameter::km3_per_s2(398600.4418);
let c1 = kep.to_cartesian(mu);
let c2 = eq.to_cartesian(mu).unwrap();
assert_approx_eq!(c1.position(), c2.position(), rtol <= 1e-8);
assert_approx_eq!(c1.velocity(), c2.velocity(), rtol <= 1e-6);
}
}