use core::marker::PhantomData;
use std::f64::consts::{PI, TAU};
use astrodyn_quantities::dims::GravParam;
use astrodyn_quantities::frame::{Planet, PlanetInertial, SelfPlanet};
use crate::error::OrbitalError;
use crate::types::{mat3_from_rows, DVec3};
#[derive(Debug, Clone)]
pub struct OrbitalElements<P: Planet> {
pub semi_major_axis: f64,
pub semiparam: f64,
pub e_mag: f64,
pub inclination: f64,
pub arg_periapsis: f64,
pub long_asc_node: f64,
pub true_anom: f64,
pub mean_anom: f64,
pub orbital_anom: f64,
pub mean_motion: f64,
pub r_mag: f64,
pub vel_mag: f64,
pub orb_energy: f64,
pub orb_ang_momentum: f64,
sin_v: f64,
cos_v: f64,
_p: PhantomData<P>,
}
impl<P: Planet> Default for OrbitalElements<P> {
fn default() -> Self {
Self {
semi_major_axis: 0.0,
semiparam: 0.0,
e_mag: 0.0,
inclination: 0.0,
arg_periapsis: 0.0,
long_asc_node: 0.0,
true_anom: 0.0,
mean_anom: 0.0,
orbital_anom: 0.0,
mean_motion: 0.0,
r_mag: 0.0,
vel_mag: 0.0,
orb_energy: 0.0,
orb_ang_momentum: 0.0,
sin_v: 0.0,
cos_v: 0.0,
_p: PhantomData,
}
}
}
const TOLERANCE: f64 = 1e-13;
const ORBIT_SWITCH_TOL: f64 = 1e-2;
fn wrap_to_tau(mut angle: f64) -> f64 {
angle %= TAU;
if angle < 0.0 {
angle += TAU;
}
angle
}
impl OrbitalElements<SelfPlanet> {
#[inline]
pub fn relabel<Q: Planet>(self) -> OrbitalElements<Q> {
OrbitalElements::<Q> {
semi_major_axis: self.semi_major_axis,
semiparam: self.semiparam,
e_mag: self.e_mag,
inclination: self.inclination,
arg_periapsis: self.arg_periapsis,
long_asc_node: self.long_asc_node,
true_anom: self.true_anom,
mean_anom: self.mean_anom,
orbital_anom: self.orbital_anom,
mean_motion: self.mean_motion,
r_mag: self.r_mag,
vel_mag: self.vel_mag,
orb_energy: self.orb_energy,
orb_ang_momentum: self.orb_ang_momentum,
sin_v: self.sin_v,
cos_v: self.cos_v,
_p: PhantomData,
}
}
}
impl OrbitalElements<SelfPlanet> {
pub(crate) fn from_cartesian_impl(
mu: f64,
pos: DVec3,
vel: DVec3,
) -> Result<OrbitalElements<SelfPlanet>, OrbitalError> {
if !mu.is_finite() || mu <= 0.0 {
return Err(OrbitalError::InvalidMu(mu));
}
let r_mag = pos.length();
let vel_mag = vel.length();
if r_mag < 1e-30 || vel_mag < 1e-30 {
return Err(OrbitalError::DegenerateOrbit);
}
let ang_momntm = pos.cross(vel);
let h_mag = ang_momntm.length();
if h_mag < 1e-30 {
return Err(OrbitalError::DegenerateOrbit);
}
let v2 = vel_mag * vel_mag;
let pos_dot_vel = pos.dot(vel);
let e_vec = ((v2 - mu / r_mag) * pos - pos_dot_vel * vel) / mu;
let ecc = e_vec.length();
let energy = v2 / 2.0 - mu / r_mag;
let (a, p, n);
if ecc < TOLERANCE {
a = r_mag;
p = r_mag;
n = (mu / a).sqrt() / a;
} else if ecc < (1.0 - ORBIT_SWITCH_TOL) {
a = -mu / (2.0 * energy);
p = a * (1.0 - ecc * ecc);
n = (mu / a).sqrt() / a;
} else if ecc > (1.0 + ORBIT_SWITCH_TOL) {
a = -mu / (2.0 * energy);
p = a * (1.0 - ecc * ecc);
n = (mu / -a).sqrt() / -a;
} else {
a = 0.0;
p = h_mag * h_mag / mu;
n = 2.0 * (mu / p).sqrt() / p;
}
let k_hat = DVec3::Z; let k_cross_h = k_hat.cross(ang_momntm);
let k_cross_h_mag = k_cross_h.length();
let k_dot_h = k_hat.dot(ang_momntm);
let incl = k_cross_h_mag.atan2(k_dot_h);
let line_of_nodes = k_cross_h;
#[allow(clippy::manual_range_contains)]
let is_equatorial = incl < TOLERANCE || (PI - TOLERANCE) < incl;
let is_circular = ecc < TOLERANCE;
let i_hat = DVec3::X;
let (lan, mut aop, mut nu);
if is_equatorial && is_circular {
lan = 0.0;
aop = 0.0;
let cross_vec = i_hat.cross(pos);
let sin_in = cross_vec.length();
let cos_in = i_hat.dot(pos);
nu = sin_in.atan2(cos_in);
if incl < TOLERANCE {
if pos.y < 0.0 {
nu = TAU - nu;
}
} else if pos.y > 0.0 {
nu = TAU - nu;
}
} else if is_equatorial {
lan = 0.0;
let cross_vec = i_hat.cross(e_vec);
let sin_in = cross_vec.length();
let cos_in = i_hat.dot(e_vec);
aop = sin_in.atan2(cos_in);
if incl < TOLERANCE {
if e_vec.y < 0.0 {
aop = TAU - aop;
}
} else if e_vec.y > 0.0 {
aop = TAU - aop;
}
let cross_vec2 = e_vec.cross(pos);
let sin_in2 = cross_vec2.length();
let cos_in2 = e_vec.dot(pos);
nu = sin_in2.atan2(cos_in2);
if pos_dot_vel < 0.0 {
nu = TAU - nu;
}
} else if is_circular {
let cross_vec = i_hat.cross(line_of_nodes);
let sin_in = cross_vec.length();
let cos_in = i_hat.dot(line_of_nodes);
lan = {
let mut v = sin_in.atan2(cos_in);
if line_of_nodes.y < 0.0 {
v = TAU - v;
}
v
};
aop = 0.0;
let cross_vec2 = line_of_nodes.cross(pos);
let sin_in2 = cross_vec2.length();
let cos_in2 = line_of_nodes.dot(pos);
nu = sin_in2.atan2(cos_in2);
if pos.z < 0.0 {
nu = TAU - nu;
}
} else {
let cross_vec = i_hat.cross(line_of_nodes);
let sin_in = cross_vec.length();
let cos_in = i_hat.dot(line_of_nodes);
lan = {
let mut v = sin_in.atan2(cos_in);
if line_of_nodes.y < 0.0 {
v = TAU - v;
}
v
};
let cross_vec2 = line_of_nodes.cross(e_vec);
let sin_in2 = cross_vec2.length();
let cos_in2 = line_of_nodes.dot(e_vec);
aop = {
let mut v = sin_in2.atan2(cos_in2);
if e_vec.z < 0.0 {
v = TAU - v;
}
v
};
let cross_vec3 = e_vec.cross(pos);
let sin_in3 = cross_vec3.length();
let cos_in3 = e_vec.dot(pos);
nu = sin_in3.atan2(cos_in3);
if pos_dot_vel < 0.0 {
nu = TAU - nu;
}
}
let sin_v = nu.sin();
let cos_v = nu.cos();
let mut oe = OrbitalElements::<SelfPlanet> {
semi_major_axis: a,
semiparam: p,
e_mag: ecc,
inclination: incl,
arg_periapsis: aop,
long_asc_node: lan,
true_anom: nu,
mean_anom: 0.0,
orbital_anom: 0.0,
mean_motion: n,
r_mag,
vel_mag,
orb_energy: energy,
orb_ang_momentum: h_mag,
sin_v,
cos_v,
_p: PhantomData,
};
oe.nu_to_anomalies();
Ok(oe)
}
}
impl<P: Planet> OrbitalElements<P> {
pub fn from_cartesian_typed(
mu: GravParam<P>,
pos: astrodyn_quantities::aliases::Position<PlanetInertial<P>>,
vel: astrodyn_quantities::aliases::Velocity<PlanetInertial<P>>,
) -> Result<OrbitalElements<P>, OrbitalError> {
OrbitalElements::<SelfPlanet>::from_cartesian_impl(mu.value, pos.raw_si(), vel.raw_si())
.map(OrbitalElements::<SelfPlanet>::relabel::<P>)
}
pub fn to_cartesian(&self, mu: f64) -> Result<(DVec3, DVec3), OrbitalError> {
if mu <= 0.0 {
return Err(OrbitalError::InvalidMu(mu));
}
let p = self.semiparam;
if p <= 0.0 || !p.is_finite() {
return Err(OrbitalError::DegenerateOrbit);
}
let e = self.e_mag;
let nu = self.true_anom;
let sin_nu = nu.sin();
let cos_nu = nu.cos();
let rss = (sin_nu * sin_nu + cos_nu * cos_nu).sqrt();
assert!(
(rss - 1.0).abs() < 1e-6,
"sin/cos of true anomaly are inconsistent: sin_v={sin_nu}, cos_v={cos_nu}, rss={rss}"
);
let denom = 1.0 + e * cos_nu;
if denom.abs() < 1e-30 {
return Err(OrbitalError::DegenerateOrbit);
}
let r = p / denom;
let r_pqw = DVec3::new(r * cos_nu, r * sin_nu, 0.0);
let coeff = (mu / p).sqrt();
let v_pqw = DVec3::new(-coeff * sin_nu, coeff * (e + cos_nu), 0.0);
let co = self.long_asc_node.cos();
let so = self.long_asc_node.sin();
let cw = self.arg_periapsis.cos();
let sw = self.arg_periapsis.sin();
let ci = self.inclination.cos();
let si = self.inclination.sin();
let row0 = DVec3::new(co * cw - so * sw * ci, -co * sw - so * cw * ci, so * si);
let row1 = DVec3::new(so * cw + co * sw * ci, -so * sw + co * cw * ci, -co * si);
let row2 = DVec3::new(sw * si, cw * si, ci);
let rot = mat3_from_rows(row0, row1, row2);
let pos = rot * r_pqw;
let vel = rot * v_pqw;
Ok((pos, vel))
}
pub fn nu_to_anomalies(&mut self) {
let e = self.e_mag;
let nu = self.true_anom;
let sin_nu = nu.sin();
let cos_nu = nu.cos();
if e < (1.0 - ORBIT_SWITCH_TOL) {
let sin_ea = ((1.0 - e * e).sqrt() * sin_nu) / (1.0 + e * cos_nu);
let cos_ea = (e + cos_nu) / (1.0 + e * cos_nu);
let ea = wrap_to_tau(sin_ea.atan2(cos_ea));
let ma = wrap_to_tau(ea - e * ea.sin());
self.orbital_anom = ea;
self.mean_anom = ma;
} else if e > (1.0 + ORBIT_SWITCH_TOL) {
let sinh_ha = ((e * e - 1.0).sqrt() * sin_nu) / (1.0 + e * cos_nu);
let cosh_ha = (e + cos_nu) / (1.0 + e * cos_nu);
let ha = (cosh_ha + sinh_ha).ln();
let ma = e * ha.sinh() - ha;
self.orbital_anom = ha;
self.mean_anom = ma;
} else {
let d = (nu / 2.0).tan();
let ma = d + d * d * d / 3.0;
self.orbital_anom = d;
self.mean_anom = ma;
}
}
pub fn mean_anom_to_nu(&mut self) -> Result<(), OrbitalError> {
let e = self.e_mag;
let m = self.mean_anom;
if e < (1.0 - ORBIT_SWITCH_TOL) {
let ea = kep_eqtn_e(m, e)?;
self.orbital_anom = ea;
let sin_ea = ea.sin();
let cos_ea = ea.cos();
let sin_nu = ((1.0 - e * e).sqrt() * sin_ea) / (1.0 - e * cos_ea);
let cos_nu = (cos_ea - e) / (1.0 - e * cos_ea);
let nu = wrap_to_tau(sin_nu.atan2(cos_nu));
self.true_anom = nu;
self.sin_v = nu.sin();
self.cos_v = nu.cos();
} else if e > (1.0 + ORBIT_SWITCH_TOL) {
let ha = kep_eqtn_h(m, e)?;
self.orbital_anom = ha;
let sinh_ha = ha.sinh();
let cosh_ha = ha.cosh();
let sin_nu = ((e * e - 1.0).sqrt() * sinh_ha) / (e * cosh_ha - 1.0);
let cos_nu = (e - cosh_ha) / (e * cosh_ha - 1.0);
let nu = wrap_to_tau(sin_nu.atan2(cos_nu));
self.true_anom = nu;
self.sin_v = nu.sin();
self.cos_v = nu.cos();
} else {
let d = kep_eqtn_b(m);
self.orbital_anom = d;
let nu = 2.0 * d.atan();
let nu = wrap_to_tau(nu);
self.true_anom = nu;
self.sin_v = nu.sin();
self.cos_v = nu.cos();
}
Ok(())
}
}
pub fn kep_eqtn_e(m: f64, e: f64) -> Result<f64, OrbitalError> {
const TOL: f64 = 1e-14;
const MAX_ITER: usize = 1000;
let mut ea = if e < 0.8 { m } else { PI };
for _ in 0..MAX_ITER {
let f = ea - e * ea.sin() - m;
let fp = 1.0 - e * ea.cos();
let delta = f / fp;
ea -= delta;
if delta.abs() < TOL {
return Ok(wrap_to_tau(ea));
}
}
Err(OrbitalError::KeplerConvergence(MAX_ITER))
}
pub fn kep_eqtn_h(m: f64, e: f64) -> Result<f64, OrbitalError> {
const TOL: f64 = 1e-14;
const MAX_ITER: usize = 1000;
let mut ha = if e < 1.6 {
if m > 0.0 {
m + e
} else {
m - e
}
} else if e < 3.6 && m.abs() > PI {
m - m.signum() * e
} else {
m / (e - 1.0)
};
for _ in 0..MAX_ITER {
let f = e * ha.sinh() - ha - m;
let fp = e * ha.cosh() - 1.0;
let delta = f / fp;
ha -= delta;
if delta.abs() < TOL {
return Ok(ha);
}
}
Err(OrbitalError::KeplerConvergence(MAX_ITER))
}
pub fn kep_eqtn_b(m: f64) -> f64 {
let disc = (9.0 * m * m / 4.0 + 1.0).sqrt();
(1.5 * m + disc).cbrt() - (-1.5 * m + disc).cbrt()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::DVec3;
impl OrbitalElements<SelfPlanet> {
pub(super) fn from_cartesian(
mu: f64,
pos: DVec3,
vel: DVec3,
) -> Result<OrbitalElements<SelfPlanet>, OrbitalError> {
Self::from_cartesian_impl(mu, pos, vel)
}
}
const MU_EARTH: f64 = 398_600.441_50;
fn roundtrip_check(mu: f64, pos: DVec3, vel: DVec3, tol: f64) {
let oe = OrbitalElements::from_cartesian(mu, pos, vel).unwrap();
let (pos2, vel2) = oe.to_cartesian(mu).unwrap();
let pos_err = (pos2 - pos).length();
let vel_err = (vel2 - vel).length();
assert!(
pos_err < tol,
"Position round-trip error {:.2e} exceeds tolerance {:.2e}\n\
pos={:?}\npos2={:?}\noe={:#?}",
pos_err,
tol,
pos,
pos2,
oe
);
assert!(
vel_err < tol,
"Velocity round-trip error {:.2e} exceeds tolerance {:.2e}\n\
vel={:?}\nvel2={:?}\noe={:#?}",
vel_err,
tol,
vel,
vel2,
oe
);
}
#[test]
fn roundtrip_circular() {
let r = 6778.0; let v = (MU_EARTH / r).sqrt(); let pos = DVec3::new(r, 0.0, 0.0);
let vel = DVec3::new(0.0, v, 0.0);
roundtrip_check(MU_EARTH, pos, vel, 1e-10);
}
#[test]
fn roundtrip_eccentric_03() {
let a = 10000.0;
let e = 0.3;
let r = a * (1.0 - e);
let v = (MU_EARTH * (1.0 + e) / (a * (1.0 - e))).sqrt();
let pos = DVec3::new(r, 0.0, 0.0);
let vel = DVec3::new(0.0, v, 0.0);
roundtrip_check(MU_EARTH, pos, vel, 1e-10);
}
#[test]
fn roundtrip_eccentric_07() {
let a = 20000.0;
let e = 0.7;
let r = a * (1.0 - e);
let v = (MU_EARTH * (1.0 + e) / (a * (1.0 - e))).sqrt();
let pos = DVec3::new(r, 0.0, 0.0);
let vel = DVec3::new(0.0, v, 0.0);
roundtrip_check(MU_EARTH, pos, vel, 1e-10);
}
#[test]
fn roundtrip_polar() {
let r = 7000.0;
let v = (MU_EARTH / r).sqrt();
let pos = DVec3::new(r, 0.0, 0.0);
let vel = DVec3::new(0.0, 0.0, v);
roundtrip_check(MU_EARTH, pos, vel, 1e-10);
}
#[test]
fn roundtrip_hyperbolic() {
let r = 7000.0;
let e = 1.5;
let a = -r / (e - 1.0); let v = (MU_EARTH * (2.0 / r - 1.0 / a)).sqrt(); let pos = DVec3::new(r, 0.0, 0.0);
let vel = DVec3::new(0.0, v, 0.0);
roundtrip_check(MU_EARTH, pos, vel, 1e-8);
}
#[test]
fn roundtrip_near_parabolic() {
let r = 7000.0;
let e = 1.0 + 1e-3;
let a = -r / (e - 1.0);
let v = (MU_EARTH * (2.0 / r - 1.0 / a)).sqrt();
let pos = DVec3::new(r, 0.0, 0.0);
let vel = DVec3::new(0.0, v, 0.0);
roundtrip_check(MU_EARTH, pos, vel, 1e-4);
}
#[test]
fn roundtrip_inclined_eccentric() {
let a = 12000.0;
let e = 0.4;
let i = 45.0_f64.to_radians();
let omega_big = 30.0_f64.to_radians();
let omega_small = 60.0_f64.to_radians();
let nu = 120.0_f64.to_radians();
let p = a * (1.0 - e * e);
let r = p / (1.0 + e * nu.cos());
let r_pqw = DVec3::new(r * nu.cos(), r * nu.sin(), 0.0);
let coeff = (MU_EARTH / p).sqrt();
let v_pqw = DVec3::new(-coeff * nu.sin(), coeff * (e + nu.cos()), 0.0);
let co = omega_big.cos();
let so = omega_big.sin();
let cw = omega_small.cos();
let sw = omega_small.sin();
let ci = i.cos();
let si = i.sin();
let row0 = DVec3::new(co * cw - so * sw * ci, -co * sw - so * cw * ci, so * si);
let row1 = DVec3::new(so * cw + co * sw * ci, -so * sw + co * cw * ci, -co * si);
let row2 = DVec3::new(sw * si, cw * si, ci);
let rot = mat3_from_rows(row0, row1, row2);
let pos = rot * r_pqw;
let vel = rot * v_pqw;
roundtrip_check(MU_EARTH, pos, vel, 1e-8);
}
#[test]
fn kepler_elliptic_m_zero() {
let ea = kep_eqtn_e(0.0, 0.5).unwrap();
assert!(
(ea).abs() < 1e-14 || (ea - TAU).abs() < 1e-14,
"E(M=0) should be 0 (or 2pi), got {}",
ea
);
}
#[test]
fn kepler_elliptic_m_pi() {
let ea = kep_eqtn_e(PI, 0.5).unwrap();
assert!(
(ea - PI).abs() < 1e-13,
"E(M=pi, e=0.5) should be ~pi, got {}",
ea
);
}
#[test]
fn kepler_elliptic_high_ecc() {
let e = 0.98;
for m in [0.01, 0.5, PI, 5.0] {
let ea = kep_eqtn_e(m, e).unwrap();
let m_check = ea - e * ea.sin();
let m_wrapped = wrap_to_tau(m);
let m_check_wrapped = wrap_to_tau(m_check);
let diff = (m_wrapped - m_check_wrapped).abs();
let diff = diff.min(TAU - diff);
assert!(
diff < 1e-13,
"Kepler check failed: M={}, e={}, E={}, M_recomputed={}",
m,
e,
ea,
m_check
);
}
}
#[test]
fn kepler_hyperbolic_convergence() {
let e = 2.0;
let m = 5.0;
let ha = kep_eqtn_h(m, e).unwrap();
let m_check = e * ha.sinh() - ha;
assert!(
(m - m_check).abs() < 1e-13,
"Hyperbolic Kepler: M={}, H={}, M_check={}",
m,
ha,
m_check
);
}
#[test]
fn kepler_hyperbolic_extreme_eccentricity() {
for (e, m) in [(10.0, 100.0), (5.0, 50.0), (1.5, 0.1), (3.0, 10.0)] {
let ha = kep_eqtn_h(m, e).unwrap();
let m_check = e * ha.sinh() - ha;
assert!(
(m - m_check).abs() < 1e-10,
"Extreme hyperbolic Kepler: e={}, M={}, H={}, M_check={}, err={}",
e,
m,
ha,
m_check,
(m - m_check).abs()
);
}
}
#[test]
fn kepler_parabolic() {
let m = 4.0 / 3.0;
let d = kep_eqtn_b(m);
let m_check = d + d * d * d / 3.0;
assert!(
(m - m_check).abs() < 1e-10,
"Parabolic Kepler: M={}, D={}, M_check={}",
m,
d,
m_check
);
}
#[test]
fn kepler_parabolic_negative_m() {
let m = -4.0 / 3.0;
let d = kep_eqtn_b(m);
let m_check = d + d * d * d / 3.0;
assert!(
(m - m_check).abs() < 1e-10,
"Parabolic Kepler negative M: M={}, D={}, M_check={}",
m,
d,
m_check
);
}
#[test]
fn near_parabolic_mean_motion_is_positive() {
let r = 7000.0;
let v = (2.0 * MU_EARTH / r).sqrt(); let pos = DVec3::new(r, 0.0, 0.0);
let vel = DVec3::new(0.0, v, 0.0);
let oe = OrbitalElements::from_cartesian(MU_EARTH, pos, vel).unwrap();
assert!(
oe.mean_motion > 0.0 && oe.mean_motion.is_finite(),
"Near-parabolic mean_motion should be positive and finite, got {}",
oe.mean_motion,
);
assert!(
(oe.e_mag - 1.0).abs() < 0.02,
"Expected e ~ 1, got {}",
oe.e_mag,
);
}
#[test]
fn true_parabolic_mean_motion_formula() {
let mu: f64 = 1.0; let r: f64 = 1.0e6; let v = (2.0 * mu / r).sqrt(); let pos = DVec3::new(r, 0.0, 0.0);
let vel = DVec3::new(0.0, v, 0.0);
let oe = OrbitalElements::from_cartesian(mu, pos, vel).unwrap();
assert_eq!(
oe.semi_major_axis, 0.0,
"Parabolic a should be 0.0 (JEOD convention: nulled out)"
);
let p = oe.semiparam;
let expected_n = 2.0 * (mu / p).sqrt() / p;
assert!(
(oe.mean_motion - expected_n).abs() / expected_n < 1e-12,
"mean_motion mismatch: got {:.6e}, expected {:.6e}",
oe.mean_motion,
expected_n,
);
}
#[test]
fn energy_check() {
let r = 7000.0;
let v = (MU_EARTH / r).sqrt() * 1.1; let pos = DVec3::new(r, 0.0, 0.0);
let vel = DVec3::new(0.0, v, 0.0);
let oe = OrbitalElements::from_cartesian(MU_EARTH, pos, vel).unwrap();
let expected_energy = v * v / 2.0 - MU_EARTH / r;
assert!(
(oe.orb_energy - expected_energy).abs() < 1e-10,
"Energy mismatch: {} vs {}",
oe.orb_energy,
expected_energy
);
}
#[test]
fn iss_orbit() {
let alt = 408.0; let r_earth = 6371.0; let r = r_earth + alt; let v = (MU_EARTH / r).sqrt();
let pos = DVec3::new(r, 0.0, 0.0);
let vel = DVec3::new(0.0, v, 0.0);
let oe = OrbitalElements::from_cartesian(MU_EARTH, pos, vel).unwrap();
assert!(
(oe.r_mag - r).abs() < 1e-8,
"r_mag should be ~{}, got {}",
r,
oe.r_mag
);
assert!(
(oe.semi_major_axis - r).abs() < 1.0, "semi_major_axis should be ~{}, got {}",
r,
oe.semi_major_axis
);
assert!(
oe.e_mag < 1e-10,
"eccentricity should be ~0, got {}",
oe.e_mag
);
}
#[test]
fn anomaly_roundtrip_elliptic() {
let a = 10000.0;
let e = 0.5;
let r = a * (1.0 - e);
let v = (MU_EARTH * (1.0 + e) / (a * (1.0 - e))).sqrt();
let pos = DVec3::new(r, 0.0, 0.0);
let vel = DVec3::new(1.0, v, 0.0);
let oe = OrbitalElements::from_cartesian(MU_EARTH, pos, vel).unwrap();
let mut oe2 = oe.clone();
oe2.mean_anom_to_nu().unwrap();
let nu_diff = (oe.true_anom - oe2.true_anom).abs();
let nu_diff = nu_diff.min(TAU - nu_diff);
assert!(
nu_diff < 1e-13,
"True anomaly round-trip error: {} (original {} vs reconstructed {})",
nu_diff,
oe.true_anom,
oe2.true_anom,
);
}
#[test]
fn roundtrip_retrograde_equatorial() {
let r = 8000.0;
let v = (MU_EARTH / r).sqrt();
let pos = DVec3::new(r, 0.0, 0.0);
let vel = DVec3::new(0.0, -v, 0.0);
roundtrip_check(MU_EARTH, pos, vel, 1e-8);
}
#[test]
fn roundtrip_nonperiapsis() {
let r = 9000.0;
let angle = PI / 4.0;
let pos = DVec3::new(r * angle.cos(), r * angle.sin(), 0.0);
let v = (MU_EARTH / r).sqrt();
let vel = DVec3::new(-v * angle.sin(), v * angle.cos(), 0.0);
roundtrip_check(MU_EARTH, pos, vel, 1e-10);
}
#[test]
fn invalid_mu() {
let pos = DVec3::new(7000.0, 0.0, 0.0);
let vel = DVec3::new(0.0, 7.0, 0.0);
assert!(OrbitalElements::from_cartesian(-1.0, pos, vel).is_err());
assert!(OrbitalElements::from_cartesian(0.0, pos, vel).is_err());
}
#[test]
fn degenerate_orbit() {
let zero = DVec3::ZERO;
let vel = DVec3::new(0.0, 7.0, 0.0);
assert!(OrbitalElements::from_cartesian(MU_EARTH, zero, vel).is_err());
}
#[test]
fn from_cartesian_known_elements() {
let a = 12000.0;
let e = 0.4;
let i = 45.0_f64.to_radians();
let omega_big = 30.0_f64.to_radians(); let omega_small = 60.0_f64.to_radians(); let nu = 120.0_f64.to_radians();
let p = a * (1.0 - e * e);
let r = p / (1.0 + e * nu.cos());
let r_pqw = DVec3::new(r * nu.cos(), r * nu.sin(), 0.0);
let coeff = (MU_EARTH / p).sqrt();
let v_pqw = DVec3::new(-coeff * nu.sin(), coeff * (e + nu.cos()), 0.0);
let co = omega_big.cos();
let so = omega_big.sin();
let cw = omega_small.cos();
let sw = omega_small.sin();
let ci = i.cos();
let si = i.sin();
let row0 = DVec3::new(co * cw - so * sw * ci, -co * sw - so * cw * ci, so * si);
let row1 = DVec3::new(so * cw + co * sw * ci, -so * sw + co * cw * ci, -co * si);
let row2 = DVec3::new(sw * si, cw * si, ci);
let rot = mat3_from_rows(row0, row1, row2);
let pos = rot * r_pqw;
let vel = rot * v_pqw;
let oe = OrbitalElements::from_cartesian(MU_EARTH, pos, vel).unwrap();
assert!(
(oe.semi_major_axis - a).abs() < 1e-8,
"semi_major_axis: expected {}, got {}",
a,
oe.semi_major_axis
);
assert!(
(oe.e_mag - e).abs() < 1e-10,
"eccentricity: expected {}, got {}",
e,
oe.e_mag
);
assert!(
(oe.inclination - i).abs() < 1e-10,
"inclination: expected {}, got {}",
i,
oe.inclination
);
assert!(
(oe.long_asc_node - omega_big).abs() < 1e-10,
"RAAN: expected {}, got {}",
omega_big,
oe.long_asc_node
);
assert!(
(oe.arg_periapsis - omega_small).abs() < 1e-10,
"arg_periapsis: expected {}, got {}",
omega_small,
oe.arg_periapsis
);
let nu_diff = (oe.true_anom - nu).abs();
let nu_diff = nu_diff.min(TAU - nu_diff);
assert!(
nu_diff < 1e-10,
"true_anom: expected {}, got {} (diff {})",
nu,
oe.true_anom,
nu_diff
);
}
#[test]
fn from_cartesian_typed_iss_like() {
use astrodyn_quantities::aliases::{Position, Velocity};
use astrodyn_quantities::ext::F64Ext;
use astrodyn_quantities::frame::{Earth, PlanetInertial};
use astrodyn_quantities::qty3::Qty3;
let mu_si: GravParam<Earth> = 3.986_004_415e14_f64.m3_per_s2_for::<Earth>();
let r = 6_779_000.0_f64; let v = (3.986_004_415e14_f64 / r).sqrt();
let pos: Position<PlanetInertial<Earth>> = Qty3::from_raw_si(DVec3::new(r, 0.0, 0.0));
let vel: Velocity<PlanetInertial<Earth>> = Qty3::from_raw_si(DVec3::new(0.0, v, 0.0));
let oe = OrbitalElements::<Earth>::from_cartesian_typed(mu_si, pos, vel).unwrap();
assert!(
oe.semi_major_axis > 6.5e6 && oe.semi_major_axis < 7.2e6,
"semi_major_axis {} out of ISS range [6.5e6, 7.2e6]",
oe.semi_major_axis
);
assert!(
oe.e_mag < 1e-10,
"eccentricity should be ~0 for circular orbit, got {}",
oe.e_mag
);
assert!(
(oe.r_mag - r).abs() < 1e-6,
"r_mag should be ~{r}, got {}",
oe.r_mag
);
}
#[test]
fn from_cartesian_typed_matches_raw_bit_for_bit() {
use astrodyn_quantities::aliases::{Position, Velocity};
use astrodyn_quantities::ext::F64Ext;
use astrodyn_quantities::frame::{Earth, PlanetInertial};
use astrodyn_quantities::qty3::Qty3;
let mu_si: GravParam<Earth> = 3.986_004_415e14_f64.m3_per_s2_for::<Earth>();
let pos_raw = DVec3::new(6_779_000.0, 0.0, 0.0);
let vel_raw = DVec3::new(0.0, 7_000.0, 1_500.0);
let pos: Position<PlanetInertial<Earth>> = Qty3::from_raw_si(pos_raw);
let vel: Velocity<PlanetInertial<Earth>> = Qty3::from_raw_si(vel_raw);
let oe_typed = OrbitalElements::<Earth>::from_cartesian_typed(mu_si, pos, vel).unwrap();
let oe_raw = OrbitalElements::from_cartesian(mu_si.value, pos_raw, vel_raw).unwrap();
assert_eq!(oe_typed.semi_major_axis, oe_raw.semi_major_axis);
assert_eq!(oe_typed.semiparam, oe_raw.semiparam);
assert_eq!(oe_typed.e_mag, oe_raw.e_mag);
assert_eq!(oe_typed.inclination, oe_raw.inclination);
assert_eq!(oe_typed.arg_periapsis, oe_raw.arg_periapsis);
assert_eq!(oe_typed.long_asc_node, oe_raw.long_asc_node);
assert_eq!(oe_typed.true_anom, oe_raw.true_anom);
assert_eq!(oe_typed.mean_anom, oe_raw.mean_anom);
assert_eq!(oe_typed.orbital_anom, oe_raw.orbital_anom);
assert_eq!(oe_typed.mean_motion, oe_raw.mean_motion);
assert_eq!(oe_typed.r_mag, oe_raw.r_mag);
assert_eq!(oe_typed.vel_mag, oe_raw.vel_mag);
assert_eq!(oe_typed.orb_energy, oe_raw.orb_energy);
assert_eq!(oe_typed.orb_ang_momentum, oe_raw.orb_ang_momentum);
}
}