use crate::astro::anomaly::{mean_to_true, true_to_mean, AnomalyError};
use crate::astro::elements::{
clamp_acos, classify, coe2rv, rv2coe, ClassicalElements, ElementsError, OrbitType,
};
use crate::astro::math::{normalize_angle, wrap_to_pi, SMALL, TWO_PI};
const PI: f64 = std::f64::consts::PI;
const HALF_PI: f64 = std::f64::consts::FRAC_PI_2;
const STABLE_HALF_ANGLE_MIN: f64 = 1.0e-4;
const STABLE_HALF_ANGLE_MAX: f64 = 1.0e4;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RetrogradeFactor {
Prograde,
Retrograde,
}
impl RetrogradeFactor {
#[inline]
fn sign(self) -> f64 {
match self {
Self::Prograde => 1.0,
Self::Retrograde => -1.0,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct EquinoctialElements {
pub a: f64,
pub h: f64,
pub k: f64,
pub p: f64,
pub q: f64,
pub lambda: f64,
pub retrograde: RetrogradeFactor,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ModifiedEquinoctialElements {
pub p: f64,
pub f: f64,
pub g: f64,
pub h: f64,
pub k: f64,
pub l: f64,
pub retrograde: RetrogradeFactor,
}
#[derive(Debug, Clone, Copy, PartialEq, thiserror::Error)]
pub enum EquinoctialError {
#[error(transparent)]
Elements(#[from] ElementsError),
#[error(transparent)]
Anomaly(#[from] AnomalyError),
#[error("inclination at the singular pole for the selected retrograde factor")]
RetrogradePole,
#[error("parabolic orbit is not representable in the equinoctial set; use MEE")]
ParabolicEquinoctial,
}
pub fn coe2eq(
coe: &ClassicalElements,
factor: RetrogradeFactor,
) -> Result<EquinoctialElements, EquinoctialError> {
validate_classical_common(coe)?;
if is_parabolic(coe.ecc) {
return Err(EquinoctialError::ParabolicEquinoctial);
}
check_finite(coe.a, "a")?;
check_retrograde_pole(coe.incl, factor)?;
let resolved = resolve_classical_angles(coe)?;
let varpi = wrap_to_pi(resolved.argp + factor.sign() * resolved.raan);
let mean = true_to_mean(resolved.nu, coe.ecc)?;
let lambda = mean_longitude(mean, varpi, coe.ecc);
let scale = inclination_scale(coe.incl, factor)?;
Ok(EquinoctialElements {
a: coe.a,
h: coe.ecc * varpi.sin(),
k: coe.ecc * varpi.cos(),
p: scale * resolved.raan.sin(),
q: scale * resolved.raan.cos(),
lambda,
retrograde: factor,
})
}
pub fn eq2coe(eq: &EquinoctialElements) -> Result<ClassicalElements, EquinoctialError> {
let recovered = recover_equinoctial(eq)?;
Ok(recovered_to_classical(&recovered))
}
pub fn coe2mee(
coe: &ClassicalElements,
factor: RetrogradeFactor,
) -> Result<ModifiedEquinoctialElements, EquinoctialError> {
validate_classical_common(coe)?;
validate_classical_a_for_mee(coe)?;
check_retrograde_pole(coe.incl, factor)?;
let resolved = resolve_classical_angles(coe)?;
let varpi = wrap_to_pi(resolved.argp + factor.sign() * resolved.raan);
let l = normalize_angle(varpi + resolved.nu);
let scale = inclination_scale(coe.incl, factor)?;
Ok(ModifiedEquinoctialElements {
p: coe.p,
f: coe.ecc * varpi.cos(),
g: coe.ecc * varpi.sin(),
h: scale * resolved.raan.cos(),
k: scale * resolved.raan.sin(),
l,
retrograde: factor,
})
}
pub fn mee2coe(mee: &ModifiedEquinoctialElements) -> Result<ClassicalElements, EquinoctialError> {
let recovered = recover_modified_equinoctial(mee)?;
Ok(recovered_to_classical(&recovered))
}
pub fn rv2eq(
r: [f64; 3],
v: [f64; 3],
mu: f64,
factor: RetrogradeFactor,
) -> Result<EquinoctialElements, EquinoctialError> {
let coe = rv2coe(r, v, mu)?;
coe2eq(&coe, factor)
}
pub fn eq2rv(eq: &EquinoctialElements, mu: f64) -> Result<([f64; 3], [f64; 3]), EquinoctialError> {
let coe = eq2coe(eq)?;
Ok(coe2rv(&coe, mu)?)
}
pub fn rv2mee(
r: [f64; 3],
v: [f64; 3],
mu: f64,
factor: RetrogradeFactor,
) -> Result<ModifiedEquinoctialElements, EquinoctialError> {
let coe = rv2coe(r, v, mu)?;
coe2mee(&coe, factor)
}
pub fn mee2rv(
mee: &ModifiedEquinoctialElements,
mu: f64,
) -> Result<([f64; 3], [f64; 3]), EquinoctialError> {
let coe = mee2coe(mee)?;
Ok(coe2rv(&coe, mu)?)
}
pub fn eq2mee(eq: &EquinoctialElements) -> Result<ModifiedEquinoctialElements, EquinoctialError> {
let recovered = recover_equinoctial(eq)?;
let scale = inclination_scale(recovered.incl, recovered.retrograde)?;
Ok(ModifiedEquinoctialElements {
p: recovered.p,
f: recovered.ecc * recovered.varpi.cos(),
g: recovered.ecc * recovered.varpi.sin(),
h: scale * recovered.raan.cos(),
k: scale * recovered.raan.sin(),
l: recovered.l,
retrograde: recovered.retrograde,
})
}
pub fn mee2eq(mee: &ModifiedEquinoctialElements) -> Result<EquinoctialElements, EquinoctialError> {
let recovered = recover_modified_equinoctial(mee)?;
if is_parabolic(recovered.ecc) {
return Err(EquinoctialError::ParabolicEquinoctial);
}
let mean = true_to_mean(recovered.nu, recovered.ecc)?;
let lambda = mean_longitude(mean, recovered.varpi, recovered.ecc);
let scale = inclination_scale(recovered.incl, recovered.retrograde)?;
Ok(EquinoctialElements {
a: recovered.a,
h: recovered.ecc * recovered.varpi.sin(),
k: recovered.ecc * recovered.varpi.cos(),
p: scale * recovered.raan.sin(),
q: scale * recovered.raan.cos(),
lambda,
retrograde: recovered.retrograde,
})
}
#[derive(Debug, Clone, Copy)]
struct ResolvedClassicalAngles {
raan: f64,
argp: f64,
nu: f64,
}
#[derive(Debug, Clone, Copy)]
struct RecoveredConic {
p: f64,
a: f64,
ecc: f64,
incl: f64,
raan: f64,
varpi: f64,
nu: f64,
l: f64,
retrograde: RetrogradeFactor,
}
fn recover_equinoctial(eq: &EquinoctialElements) -> Result<RecoveredConic, EquinoctialError> {
validate_equinoctial(eq)?;
let ecc = eq.h.hypot(eq.k);
if is_parabolic(ecc) {
return Err(EquinoctialError::ParabolicEquinoctial);
}
let varpi = eq.h.atan2(eq.k);
let incl = recover_inclination(eq.p, eq.q, eq.retrograde)?;
let raan = eq.p.atan2(eq.q);
let mean = eq.lambda - varpi;
let nu = mean_to_true(mean, ecc)?;
let l = normalize_angle(varpi + nu);
let p = eq.a * (1.0 - ecc * ecc);
if !p.is_finite() {
return Err(ElementsError::NonFinite { field: "p" }.into());
}
if p <= 0.0 {
return Err(ElementsError::NonPositiveSemiLatus.into());
}
Ok(RecoveredConic {
p,
a: eq.a,
ecc,
incl,
raan,
varpi,
nu,
l,
retrograde: eq.retrograde,
})
}
fn recover_modified_equinoctial(
mee: &ModifiedEquinoctialElements,
) -> Result<RecoveredConic, EquinoctialError> {
validate_modified_equinoctial(mee)?;
let ecc = mee.f.hypot(mee.g);
let varpi = mee.g.atan2(mee.f);
let incl = recover_inclination(mee.h, mee.k, mee.retrograde)?;
let raan = mee.k.atan2(mee.h);
let nu = mee.l - varpi;
let a = if is_parabolic(ecc) {
f64::INFINITY
} else {
mee.p / (1.0 - ecc * ecc)
};
if !is_parabolic(ecc) && !a.is_finite() {
return Err(ElementsError::NonFinite { field: "a" }.into());
}
Ok(RecoveredConic {
p: mee.p,
a,
ecc,
incl,
raan,
varpi,
nu,
l: normalize_angle(mee.l),
retrograde: mee.retrograde,
})
}
fn recovered_to_classical(recovered: &RecoveredConic) -> ClassicalElements {
let orbit_type = classify(recovered.ecc, recovered.incl);
let circular = matches!(
orbit_type,
OrbitType::CircularInclined | OrbitType::CircularEquatorial
);
let equatorial = matches!(
orbit_type,
OrbitType::EllipticalEquatorial | OrbitType::CircularEquatorial
);
let ecc = if circular { 0.0 } else { recovered.ecc };
let incl = if equatorial {
if recovered.incl > HALF_PI {
PI
} else {
0.0
}
} else {
recovered.incl
};
let (p, a) = if circular && recovered.a.is_finite() {
(recovered.a, recovered.a)
} else {
(recovered.p, recovered.a)
};
let raan = normalize_angle(recovered.raan);
let argp = normalize_angle(recovered.varpi - recovered.retrograde.sign() * raan);
let nu = normalize_angle(recovered.nu);
let l = normalize_angle(recovered.l);
match orbit_type {
OrbitType::EllipticalInclined => ClassicalElements {
p,
a,
ecc,
incl,
raan,
argp,
nu,
arglat: f64::NAN,
truelon: f64::NAN,
lonper: f64::NAN,
orbit_type,
},
OrbitType::CircularInclined => ClassicalElements {
p,
a,
ecc,
incl,
raan,
argp: f64::NAN,
nu: f64::NAN,
arglat: normalize_angle(l - recovered.retrograde.sign() * raan),
truelon: f64::NAN,
lonper: f64::NAN,
orbit_type,
},
OrbitType::EllipticalEquatorial => ClassicalElements {
p,
a,
ecc,
incl,
raan: f64::NAN,
argp: f64::NAN,
nu,
arglat: f64::NAN,
truelon: f64::NAN,
lonper: folded_equatorial_angle(recovered.varpi, incl),
orbit_type,
},
OrbitType::CircularEquatorial => ClassicalElements {
p,
a,
ecc,
incl,
raan: f64::NAN,
argp: f64::NAN,
nu: f64::NAN,
arglat: f64::NAN,
truelon: folded_equatorial_angle(l, incl),
lonper: f64::NAN,
orbit_type,
},
}
}
fn resolve_classical_angles(
coe: &ClassicalElements,
) -> Result<ResolvedClassicalAngles, EquinoctialError> {
match coe.orbit_type {
OrbitType::EllipticalInclined => {
check_finite(coe.raan, "raan")?;
check_finite(coe.argp, "argp")?;
check_finite(coe.nu, "nu")?;
Ok(ResolvedClassicalAngles {
raan: coe.raan,
argp: coe.argp,
nu: coe.nu,
})
}
OrbitType::CircularInclined => {
check_finite(coe.raan, "raan")?;
check_finite(coe.arglat, "arglat")?;
Ok(ResolvedClassicalAngles {
raan: coe.raan,
argp: 0.0,
nu: coe.arglat,
})
}
OrbitType::EllipticalEquatorial => {
check_finite(coe.lonper, "lonper")?;
check_finite(coe.nu, "nu")?;
Ok(ResolvedClassicalAngles {
raan: 0.0,
argp: unfolded_equatorial_angle(coe.lonper, coe.incl),
nu: coe.nu,
})
}
OrbitType::CircularEquatorial => {
check_finite(coe.truelon, "truelon")?;
Ok(ResolvedClassicalAngles {
raan: 0.0,
argp: 0.0,
nu: unfolded_equatorial_angle(coe.truelon, coe.incl),
})
}
}
}
fn validate_classical_common(coe: &ClassicalElements) -> Result<(), EquinoctialError> {
check_finite(coe.p, "p")?;
if coe.p <= 0.0 {
return Err(ElementsError::NonPositiveSemiLatus.into());
}
check_finite(coe.ecc, "ecc")?;
check_finite(coe.incl, "incl")?;
Ok(())
}
fn validate_classical_a_for_mee(coe: &ClassicalElements) -> Result<(), EquinoctialError> {
if is_parabolic(coe.ecc) {
Ok(())
} else {
check_finite(coe.a, "a")
}
}
fn validate_equinoctial(eq: &EquinoctialElements) -> Result<(), EquinoctialError> {
check_finite(eq.a, "a")?;
check_finite(eq.h, "h")?;
check_finite(eq.k, "k")?;
check_finite(eq.p, "p")?;
check_finite(eq.q, "q")?;
check_finite(eq.lambda, "lambda")
}
fn validate_modified_equinoctial(
mee: &ModifiedEquinoctialElements,
) -> Result<(), EquinoctialError> {
check_finite(mee.p, "p")?;
if mee.p <= 0.0 {
return Err(ElementsError::NonPositiveSemiLatus.into());
}
check_finite(mee.f, "f")?;
check_finite(mee.g, "g")?;
check_finite(mee.h, "h")?;
check_finite(mee.k, "k")?;
check_finite(mee.l, "l")
}
fn check_finite(value: f64, field: &'static str) -> Result<(), EquinoctialError> {
if value.is_finite() {
Ok(())
} else {
Err(ElementsError::NonFinite { field }.into())
}
}
fn inclination_scale(incl: f64, factor: RetrogradeFactor) -> Result<f64, EquinoctialError> {
check_retrograde_pole(incl, factor)?;
let tan_half = (0.5 * incl).tan();
let scale = match factor {
RetrogradeFactor::Prograde => tan_half,
RetrogradeFactor::Retrograde => 1.0 / tan_half,
};
if scale.is_finite() {
Ok(scale)
} else {
Err(EquinoctialError::RetrogradePole)
}
}
fn recover_inclination(x: f64, y: f64, factor: RetrogradeFactor) -> Result<f64, EquinoctialError> {
let scale = x.hypot(y);
if !scale.is_finite() {
return Err(EquinoctialError::RetrogradePole);
}
let atan_incl = match factor {
RetrogradeFactor::Prograde => 2.0 * scale.atan(),
RetrogradeFactor::Retrograde => PI - 2.0 * scale.atan(),
};
let incl = if (STABLE_HALF_ANGLE_MIN..=STABLE_HALF_ANGLE_MAX).contains(&scale) {
let scale_sq = scale * scale;
let cos_incl = match factor {
RetrogradeFactor::Prograde => (1.0 - scale_sq) / (1.0 + scale_sq),
RetrogradeFactor::Retrograde => (scale_sq - 1.0) / (1.0 + scale_sq),
};
clamp_acos(cos_incl)
} else {
atan_incl
};
check_retrograde_pole(incl, factor)?;
Ok(incl)
}
fn check_retrograde_pole(incl: f64, factor: RetrogradeFactor) -> Result<(), EquinoctialError> {
match factor {
RetrogradeFactor::Prograde if (incl - PI).abs() < SMALL => {
Err(EquinoctialError::RetrogradePole)
}
RetrogradeFactor::Retrograde if incl.abs() < SMALL => Err(EquinoctialError::RetrogradePole),
_ => Ok(()),
}
}
fn folded_equatorial_angle(angle: f64, incl: f64) -> f64 {
let angle = normalize_angle(angle);
if incl > HALF_PI {
normalize_angle(TWO_PI - angle)
} else {
angle
}
}
fn unfolded_equatorial_angle(angle: f64, incl: f64) -> f64 {
folded_equatorial_angle(angle, incl)
}
fn mean_longitude(mean: f64, varpi: f64, ecc: f64) -> f64 {
if ecc < 1.0 {
normalize_angle(mean + varpi)
} else {
mean + varpi
}
}
fn is_parabolic(ecc: f64) -> bool {
(ecc - 1.0).abs() < SMALL
}
#[cfg(test)]
mod tests {
use super::*;
const MU: f64 = 398600.4418;
const DEG: f64 = std::f64::consts::PI / 180.0;
#[derive(Debug, Clone, Copy)]
struct Case {
name: &'static str,
coe: ClassicalElements,
factor: RetrogradeFactor,
}
fn assert_close(got: f64, want: f64, tol: f64, label: &str) {
assert!(
(got - want).abs() <= tol,
"{label}: got {got}, want {want}, diff {}",
(got - want).abs()
);
}
fn assert_scaled_close(got: f64, want: f64, rel: f64, label: &str) {
let scale = 1.0_f64.max(want.abs());
assert!(
(got - want).abs() <= rel * scale,
"{label}: got {got}, want {want}, diff {}",
(got - want).abs()
);
}
fn angle_diff(a: f64, b: f64) -> f64 {
let diff = normalize_angle(a - b);
diff.min(TWO_PI - diff)
}
fn assert_angle_close(got: f64, want: f64, tol: f64, label: &str) {
let diff = angle_diff(got, want);
assert!(diff <= tol, "{label}: got {got}, want {want}, diff {diff}");
}
fn assert_vec_close(got: [f64; 3], want: [f64; 3], tol: f64, label: &str) {
for i in 0..3 {
assert!(
(got[i] - want[i]).abs() <= tol,
"{label}[{i}]: got {}, want {}, diff {}",
got[i],
want[i],
(got[i] - want[i]).abs()
);
}
}
fn assert_state_close(got: ([f64; 3], [f64; 3]), want: ([f64; 3], [f64; 3]), label: &str) {
assert_vec_close(got.0, want.0, 1.0e-7, &format!("{label} r"));
assert_vec_close(got.1, want.1, 1.0e-10, &format!("{label} v"));
}
fn classical(p: f64, ecc: f64, incl: f64, raan: f64, argp: f64, nu: f64) -> ClassicalElements {
ClassicalElements::new(p, ecc, incl, raan, argp, nu)
}
fn circular_inclined(p: f64, incl: f64, raan: f64, arglat: f64) -> ClassicalElements {
let mut coe = ClassicalElements::new(p, 0.0, incl, raan, 0.0, 0.0);
coe.orbit_type = OrbitType::CircularInclined;
coe.argp = f64::NAN;
coe.nu = f64::NAN;
coe.arglat = arglat;
coe
}
fn elliptical_equatorial(
p: f64,
ecc: f64,
incl: f64,
lonper: f64,
nu: f64,
) -> ClassicalElements {
let mut coe = ClassicalElements::new(p, ecc, incl, 0.0, 0.0, nu);
coe.orbit_type = OrbitType::EllipticalEquatorial;
coe.raan = f64::NAN;
coe.argp = f64::NAN;
coe.lonper = lonper;
coe
}
fn regimes() -> Vec<Case> {
vec![
Case {
name: "leo circular inclined",
coe: circular_inclined(7000.0, 51.6 * DEG, 80.0 * DEG, 135.0 * DEG),
factor: RetrogradeFactor::Prograde,
},
Case {
name: "geo near equatorial",
coe: classical(
42164.0 * (1.0 - 0.001_f64.powi(2)),
0.001,
0.01 * DEG,
40.0 * DEG,
20.0 * DEG,
10.0 * DEG,
),
factor: RetrogradeFactor::Prograde,
},
Case {
name: "molniya",
coe: classical(
26600.0 * (1.0 - 0.74_f64.powi(2)),
0.74,
63.4 * DEG,
30.0 * DEG,
270.0 * DEG,
15.0 * DEG,
),
factor: RetrogradeFactor::Prograde,
},
Case {
name: "near circular",
coe: classical(
7200.0 * (1.0 - 1.0e-6_f64.powi(2)),
1.0e-6,
40.0 * DEG,
12.0 * DEG,
25.0 * DEG,
80.0 * DEG,
),
factor: RetrogradeFactor::Prograde,
},
Case {
name: "near equatorial",
coe: classical(
9000.0 * (1.0 - 0.2_f64.powi(2)),
0.2,
1.0e-4 * DEG,
90.0 * DEG,
80.0 * DEG,
70.0 * DEG,
),
factor: RetrogradeFactor::Prograde,
},
Case {
name: "combined near circular near equatorial",
coe: classical(
7100.0 * (1.0 - 1.0e-6_f64.powi(2)),
1.0e-6,
1.0e-4 * DEG,
5.0 * DEG,
5.0 * DEG,
45.0 * DEG,
),
factor: RetrogradeFactor::Prograde,
},
Case {
name: "retrograde prograde factor",
coe: classical(
12000.0 * (1.0 - 0.1_f64.powi(2)),
0.1,
175.0 * DEG,
45.0 * DEG,
10.0 * DEG,
250.0 * DEG,
),
factor: RetrogradeFactor::Prograde,
},
Case {
name: "true retrograde",
coe: elliptical_equatorial(
11000.0 * (1.0 - 0.2_f64.powi(2)),
0.2,
PI,
40.0 * DEG,
80.0 * DEG,
),
factor: RetrogradeFactor::Retrograde,
},
]
}
fn assert_classical_recombined_close(
got: &ClassicalElements,
want: &ClassicalElements,
factor: RetrogradeFactor,
label: &str,
) {
assert_eq!(got.orbit_type, want.orbit_type, "{label} orbit_type");
assert_scaled_close(got.p, want.p, 1.0e-10, &format!("{label} p"));
if got.a.is_finite() && want.a.is_finite() {
assert_scaled_close(got.a, want.a, 1.0e-10, &format!("{label} a"));
} else {
assert_eq!(got.a.is_infinite(), want.a.is_infinite(), "{label} a inf");
}
assert_close(got.ecc, want.ecc, 1.0e-10, &format!("{label} ecc"));
assert_angle_close(got.incl, want.incl, 1.0e-10, &format!("{label} incl"));
let got_resolved = resolve_classical_angles(got).unwrap();
let want_resolved = resolve_classical_angles(want).unwrap();
let got_varpi = wrap_to_pi(got_resolved.argp + factor.sign() * got_resolved.raan);
let want_varpi = wrap_to_pi(want_resolved.argp + factor.sign() * want_resolved.raan);
assert_angle_close(got_varpi, want_varpi, 1.0e-10, &format!("{label} varpi"));
assert_angle_close(
normalize_angle(got_varpi + got_resolved.nu),
normalize_angle(want_varpi + want_resolved.nu),
1.0e-10,
&format!("{label} L"),
);
}
fn assert_eq_close(
got: &EquinoctialElements,
want: &EquinoctialElements,
ecc: f64,
label: &str,
) {
assert_scaled_close(got.a, want.a, 1.0e-10, &format!("{label} a"));
assert_close(got.h, want.h, 1.0e-10, &format!("{label} h"));
assert_close(got.k, want.k, 1.0e-10, &format!("{label} k"));
assert_close(got.p, want.p, 1.0e-10, &format!("{label} p"));
assert_close(got.q, want.q, 1.0e-10, &format!("{label} q"));
if ecc > 1.0 {
assert_scaled_close(got.lambda, want.lambda, 1.0e-9, &format!("{label} lambda"));
} else {
assert_angle_close(got.lambda, want.lambda, 1.0e-10, &format!("{label} lambda"));
}
assert_eq!(got.retrograde, want.retrograde, "{label} factor");
}
fn assert_mee_close(
got: &ModifiedEquinoctialElements,
want: &ModifiedEquinoctialElements,
label: &str,
) {
assert_scaled_close(got.p, want.p, 1.0e-10, &format!("{label} p"));
assert_close(got.f, want.f, 1.0e-10, &format!("{label} f"));
assert_close(got.g, want.g, 1.0e-10, &format!("{label} g"));
assert_close(got.h, want.h, 1.0e-10, &format!("{label} h"));
assert_close(got.k, want.k, 1.0e-10, &format!("{label} k"));
assert_angle_close(got.l, want.l, 1.0e-10, &format!("{label} l"));
assert_eq!(got.retrograde, want.retrograde, "{label} factor");
}
#[test]
fn round_trip_state_across_regimes() {
for case in regimes() {
let state = coe2rv(&case.coe, MU).unwrap();
let eq = rv2eq(state.0, state.1, MU, case.factor).unwrap();
let eq_state = eq2rv(&eq, MU).unwrap();
assert_state_close(eq_state, state, case.name);
let mee = rv2mee(state.0, state.1, MU, case.factor).unwrap();
let mee_state = mee2rv(&mee, MU).unwrap();
assert_state_close(mee_state, state, case.name);
}
}
#[test]
fn round_trip_element_algebra_across_regimes() {
for case in regimes() {
let eq = coe2eq(&case.coe, case.factor).unwrap();
let eq_back = eq2coe(&eq).unwrap();
assert_classical_recombined_close(&eq_back, &case.coe, case.factor, case.name);
let mee = coe2mee(&case.coe, case.factor).unwrap();
let mee_back = mee2coe(&mee).unwrap();
assert_classical_recombined_close(&mee_back, &case.coe, case.factor, case.name);
let converted_mee = eq2mee(&eq).unwrap();
assert_mee_close(&converted_mee, &mee, case.name);
let converted_eq = mee2eq(&mee).unwrap();
assert_eq_close(&converted_eq, &eq, case.coe.ecc, case.name);
}
}
#[test]
fn vallado_reference_components_match_full_precision_and_rounded_values() {
let r = [6524.834, 6862.875, 6448.296];
let v = [4.901327, 5.533756, -1.976341];
let coe = rv2coe(r, v, MU).unwrap();
let eq = coe2eq(&coe, RetrogradeFactor::Prograde).unwrap();
let mee = coe2mee(&coe, RetrogradeFactor::Prograde).unwrap();
let varpi = wrap_to_pi(coe.argp + coe.raan);
let mean = true_to_mean(coe.nu, coe.ecc).unwrap();
let scale = (0.5 * coe.incl).tan();
assert_close(eq.a, coe.a, 1.0e-12, "eq a full");
assert_close(eq.h, coe.ecc * varpi.sin(), 1.0e-12, "eq h full");
assert_close(eq.k, coe.ecc * varpi.cos(), 1.0e-12, "eq k full");
assert_close(eq.p, scale * coe.raan.sin(), 1.0e-12, "eq p full");
assert_close(eq.q, scale * coe.raan.cos(), 1.0e-12, "eq q full");
assert_angle_close(eq.lambda, mean + varpi, 1.0e-12, "eq lambda full");
assert_close(mee.p, coe.p, 1.0e-12, "mee p full");
assert_close(mee.f, coe.ecc * varpi.cos(), 1.0e-12, "mee f full");
assert_close(mee.g, coe.ecc * varpi.sin(), 1.0e-12, "mee g full");
assert_close(mee.h, scale * coe.raan.cos(), 1.0e-12, "mee h full");
assert_close(mee.k, scale * coe.raan.sin(), 1.0e-12, "mee k full");
assert_angle_close(mee.l, varpi + coe.nu, 1.0e-12, "mee l full");
let e = 0.832853_f64;
let incl = 87.870 * DEG;
let raan = 227.898 * DEG;
let argp = 53.38 * DEG;
let nu = 92.335 * DEG;
let varpi = wrap_to_pi(argp + raan);
let mean = true_to_mean(nu, e).unwrap();
let scale = (0.5 * incl).tan();
assert_close(eq.a, 36127.343, 1.0e-2, "eq a rounded");
assert_close(eq.h, e * varpi.sin(), 1.0e-4, "eq h rounded");
assert_close(eq.k, e * varpi.cos(), 1.0e-4, "eq k rounded");
assert_close(eq.p, scale * raan.sin(), 1.0e-4, "eq p rounded");
assert_close(eq.q, scale * raan.cos(), 1.0e-4, "eq q rounded");
assert_angle_close(eq.lambda, mean + varpi, 1.0e-4, "eq lambda rounded");
assert_close(mee.p, 11067.790, 1.0e-2, "mee p rounded");
assert_close(mee.f, e * varpi.cos(), 1.0e-4, "mee f rounded");
assert_close(mee.g, e * varpi.sin(), 1.0e-4, "mee g rounded");
assert_close(mee.h, scale * raan.cos(), 1.0e-4, "mee h rounded");
assert_close(mee.k, scale * raan.sin(), 1.0e-4, "mee k rounded");
assert_angle_close(mee.l, varpi + nu, 1.0e-4, "mee l rounded");
}
#[test]
fn mee2rv_near_equatorial_low_eccentricity_regression() {
let coe = classical(
7000.0 * (1.0 - 1.0e-4_f64.powi(2)),
1.0e-4,
1.0e-3 * DEG,
15.0 * DEG,
25.0 * DEG,
35.0 * DEG,
);
let state = coe2rv(&coe, MU).unwrap();
let mee = coe2mee(&coe, RetrogradeFactor::Prograde).unwrap();
assert_state_close(mee2rv(&mee, MU).unwrap(), state, "near equatorial mee2rv");
}
#[test]
fn retrograde_pole_errors_and_opposite_factor_round_trips() {
let coe = elliptical_equatorial(
11000.0 * (1.0 - 0.2_f64.powi(2)),
0.2,
PI,
40.0 * DEG,
80.0 * DEG,
);
assert_eq!(
coe2eq(&coe, RetrogradeFactor::Prograde),
Err(EquinoctialError::RetrogradePole)
);
assert_eq!(
coe2mee(&coe, RetrogradeFactor::Prograde),
Err(EquinoctialError::RetrogradePole)
);
let state = coe2rv(&coe, MU).unwrap();
let eq = coe2eq(&coe, RetrogradeFactor::Retrograde).unwrap();
assert_state_close(eq2rv(&eq, MU).unwrap(), state, "retrograde eq");
let mee = coe2mee(&coe, RetrogradeFactor::Retrograde).unwrap();
assert_state_close(mee2rv(&mee, MU).unwrap(), state, "retrograde mee");
}
#[test]
fn parabolic_equinoctial_rejected_and_mee_round_trips() {
let coe = classical(12000.0, 1.0, 30.0 * DEG, 40.0 * DEG, 50.0 * DEG, 60.0 * DEG);
assert_eq!(
coe2eq(&coe, RetrogradeFactor::Prograde),
Err(EquinoctialError::ParabolicEquinoctial)
);
let state = coe2rv(&coe, MU).unwrap();
assert_eq!(
rv2eq(state.0, state.1, MU, RetrogradeFactor::Prograde),
Err(EquinoctialError::ParabolicEquinoctial)
);
let mee = coe2mee(&coe, RetrogradeFactor::Prograde).unwrap();
assert_close(mee.p, coe.p, 1.0e-12, "mee parabolic p");
let back = mee2coe(&mee).unwrap();
assert!(back.a.is_infinite());
assert_close(back.p, coe.p, 1.0e-12, "coe parabolic p");
assert_state_close(mee2rv(&mee, MU).unwrap(), state, "parabolic mee");
}
#[test]
fn input_rejections_are_typed() {
let coe = classical(
7200.0 * (1.0 - 0.01_f64.powi(2)),
0.01,
40.0 * DEG,
12.0 * DEG,
25.0 * DEG,
80.0 * DEG,
);
let state = coe2rv(&coe, MU).unwrap();
let mut eq = coe2eq(&coe, RetrogradeFactor::Prograde).unwrap();
eq.h = f64::NAN;
assert!(matches!(
eq2coe(&eq),
Err(EquinoctialError::Elements(ElementsError::NonFinite {
field: "h"
}))
));
let mut mee = coe2mee(&coe, RetrogradeFactor::Prograde).unwrap();
mee.l = f64::INFINITY;
assert!(matches!(
mee2coe(&mee),
Err(EquinoctialError::Elements(ElementsError::NonFinite {
field: "l"
}))
));
let eq = coe2eq(&coe, RetrogradeFactor::Prograde).unwrap();
assert_eq!(
eq2rv(&eq, 0.0),
Err(EquinoctialError::Elements(ElementsError::NonPositiveMu))
);
assert_eq!(
rv2eq(state.0, state.1, 0.0, RetrogradeFactor::Prograde),
Err(EquinoctialError::Elements(ElementsError::NonPositiveMu))
);
assert_eq!(
rv2eq([0.0, 0.0, 0.0], state.1, MU, RetrogradeFactor::Prograde),
Err(EquinoctialError::Elements(ElementsError::ZeroPosition))
);
assert_eq!(
rv2mee(
[7000.0, 0.0, 0.0],
[7.5, 0.0, 0.0],
MU,
RetrogradeFactor::Prograde
),
Err(EquinoctialError::Elements(ElementsError::DegenerateOrbit))
);
let hyperbolic = classical(10000.0, 1.5, 40.0 * DEG, 20.0 * DEG, 30.0 * DEG, PI);
assert!(matches!(
coe2eq(&hyperbolic, RetrogradeFactor::Prograde),
Err(EquinoctialError::Anomaly(
AnomalyError::BeyondAsymptote { .. }
))
));
}
}