use crate::core::error::{PoliastroError, PoliastroResult};
use crate::core::linalg::{Vector3, Matrix3};
use pyo3::prelude::*;
use std::f64::consts::PI;
pub const DEFAULT_TOL: f64 = 1e-8;
#[pyclass(module = "astrora._core", name = "OrbitalElements")]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct OrbitalElements {
#[pyo3(get, set)]
pub a: f64,
#[pyo3(get, set)]
pub e: f64,
#[pyo3(get, set)]
pub i: f64,
#[pyo3(get, set)]
pub raan: f64,
#[pyo3(get, set)]
pub argp: f64,
#[pyo3(get, set)]
pub nu: f64,
}
impl OrbitalElements {
pub fn new(a: f64, e: f64, i: f64, raan: f64, argp: f64, nu: f64) -> Self {
Self {
a,
e,
i,
raan,
argp,
nu,
}
}
pub fn period(&self, mu: f64) -> PoliastroResult<f64> {
if self.e >= 1.0 {
return Err(PoliastroError::UnsupportedOrbitType {
operation: "period calculation".into(),
orbit_type: if self.e > 1.0 {
"hyperbolic"
} else {
"parabolic"
}
.into(),
});
}
Ok(2.0 * PI * (self.a.powi(3) / mu).sqrt())
}
pub fn periapsis(&self) -> f64 {
self.a * (1.0 - self.e)
}
pub fn apoapsis(&self) -> f64 {
self.a * (1.0 + self.e)
}
pub fn semi_latus_rectum(&self) -> f64 {
self.a * (1.0 - self.e * self.e)
}
pub fn specific_angular_momentum_magnitude(&self, mu: f64) -> f64 {
(mu * self.semi_latus_rectum()).sqrt()
}
}
#[pyclass(module = "astrora._core", name = "EquinoctialElements")]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct EquinoctialElements {
#[pyo3(get, set)]
pub p: f64,
#[pyo3(get, set)]
pub f: f64,
#[pyo3(get, set)]
pub g: f64,
#[pyo3(get, set)]
pub h: f64,
#[pyo3(get, set)]
pub k: f64,
#[pyo3(get, set)]
pub L: f64,
}
impl EquinoctialElements {
pub fn new(p: f64, f: f64, g: f64, h: f64, k: f64, L: f64) -> Self {
Self { p, f, g, h, k, L }
}
pub fn eccentricity(&self) -> f64 {
(self.f * self.f + self.g * self.g).sqrt()
}
pub fn semi_major_axis(&self) -> PoliastroResult<f64> {
let e = self.eccentricity();
if e >= 1.0 {
return Err(PoliastroError::UnsupportedOrbitType {
operation: "semi-major axis calculation".into(),
orbit_type: if e > 1.0 {
"hyperbolic"
} else {
"parabolic"
}
.into(),
});
}
Ok(self.p / (1.0 - e * e))
}
pub fn inclination(&self) -> f64 {
2.0 * (self.h * self.h + self.k * self.k).sqrt().atan()
}
pub fn period(&self, mu: f64) -> PoliastroResult<f64> {
let a = self.semi_major_axis()?;
Ok(2.0 * PI * (a.powi(3) / mu).sqrt())
}
}
pub fn rv_to_coe(
r: &Vector3,
v: &Vector3,
mu: f64,
tol: f64,
) -> PoliastroResult<OrbitalElements> {
let r_mag = r.norm();
let v_mag = v.norm();
let v_r = r.dot(v) / r_mag;
let h_vec = r.cross(v);
let h_mag = h_vec.norm();
if h_mag < tol {
return Err(PoliastroError::InvalidStateVector {
reason: format!("orbit is degenerate (zero angular momentum): |h| = {h_mag}"),
});
}
let i = (h_vec.z / h_mag).acos();
let k_vec = Vector3::new(0.0, 0.0, 1.0);
let n_vec = k_vec.cross(&h_vec);
let n_mag = n_vec.norm();
let ecc_vec = ((v_mag * v_mag - mu / r_mag) * r - (r.dot(v)) * v) / mu;
let e = ecc_vec.norm();
let energy = v_mag * v_mag / 2.0 - mu / r_mag;
let a = if e.abs() < 1.0 - tol {
-mu / (2.0 * energy)
} else if e.abs() < 1.0 + tol {
return Err(PoliastroError::UnsupportedOrbitType {
operation: "rv_to_coe".into(),
orbit_type: "parabolic (e ≈ 1)".into(),
});
} else {
-mu / (2.0 * energy) };
let raan = if i.abs() < tol || (PI - i).abs() < tol {
0.0
} else if n_mag < tol {
0.0
} else {
let raan_base = (n_vec.x / n_mag).acos();
if n_vec.y >= 0.0 {
raan_base
} else {
2.0 * PI - raan_base
}
};
let argp = if e < tol {
0.0
} else if i.abs() < tol || (PI - i).abs() < tol {
let argp_base = (ecc_vec.x / e).acos();
if ecc_vec.y >= 0.0 {
argp_base
} else {
2.0 * PI - argp_base
}
} else if n_mag < tol {
0.0
} else {
let argp_base = (n_vec.dot(&ecc_vec) / (n_mag * e)).acos();
if ecc_vec.z >= 0.0 {
argp_base
} else {
2.0 * PI - argp_base
}
};
let nu = if e < tol {
if i.abs() < tol || (PI - i).abs() < tol {
let nu_base = (r.x / r_mag).acos();
if r.y >= 0.0 {
nu_base
} else {
2.0 * PI - nu_base
}
} else if n_mag < tol {
0.0
} else {
let nu_base = (n_vec.dot(r) / (n_mag * r_mag)).acos();
if r.z >= 0.0 {
nu_base
} else {
2.0 * PI - nu_base
}
}
} else {
let nu_base = (ecc_vec.dot(r) / (e * r_mag)).acos();
if v_r >= 0.0 {
nu_base
} else {
2.0 * PI - nu_base
}
};
Ok(OrbitalElements::new(a, e, i, raan, argp, nu))
}
pub fn coe_to_rv(elements: &OrbitalElements, mu: f64) -> (Vector3, Vector3) {
let OrbitalElements {
a,
e,
i,
raan,
argp,
nu,
} = *elements;
let p = a * (1.0 - e * e);
let r_mag = p / (1.0 + e * nu.cos());
let r_pqw = Vector3::new(r_mag * nu.cos(), r_mag * nu.sin(), 0.0);
let h = (mu * p).sqrt();
let v_pqw = (mu / h) * Vector3::new(-nu.sin(), e + nu.cos(), 0.0);
let r_z_argp = Matrix3::new(
argp.cos(),
-argp.sin(),
0.0,
argp.sin(),
argp.cos(),
0.0,
0.0,
0.0,
1.0,
);
let r_x_i = Matrix3::new(1.0, 0.0, 0.0, 0.0, i.cos(), -i.sin(), 0.0, i.sin(), i.cos());
let r_z_raan = Matrix3::new(
raan.cos(),
-raan.sin(),
0.0,
raan.sin(),
raan.cos(),
0.0,
0.0,
0.0,
1.0,
);
let rotation = r_z_raan * r_x_i * r_z_argp;
let r_inertial = rotation * r_pqw;
let v_inertial = rotation * v_pqw;
(r_inertial, v_inertial)
}
pub fn coe_to_equinoctial(elements: &OrbitalElements) -> EquinoctialElements {
let OrbitalElements {
a,
e,
i,
raan,
argp,
nu,
} = *elements;
let p = a * (1.0 - e * e);
let omega_plus_raan = argp + raan;
let f = e * omega_plus_raan.cos();
let g = e * omega_plus_raan.sin();
let tan_half_i = (i / 2.0).tan();
let h = tan_half_i * raan.cos();
let k = tan_half_i * raan.sin();
let L = raan + argp + nu;
EquinoctialElements::new(p, f, g, h, k, L)
}
pub fn equinoctial_to_coe(
elements: &EquinoctialElements,
tol: f64,
) -> PoliastroResult<OrbitalElements> {
let EquinoctialElements {
p,
f,
g,
h,
k,
L,
} = *elements;
let e = (f * f + g * g).sqrt();
if e >= 1.0 - tol {
return Err(PoliastroError::UnsupportedOrbitType {
operation: "equinoctial_to_coe".into(),
orbit_type: if e > 1.0 + tol {
"hyperbolic (e > 1)"
} else {
"parabolic (e ≈ 1)"
}
.into(),
});
}
let a = p / (1.0 - e * e);
let i = 2.0 * (h * h + k * k).sqrt().atan();
let (raan, argp, nu) = if i.abs() < tol {
let raan = 0.0;
if e < tol {
let argp = 0.0;
let nu = normalize_angle(L);
(raan, argp, nu)
} else {
let argp = normalize_angle(g.atan2(f));
let nu = normalize_angle(L - argp);
(raan, argp, nu)
}
} else if e < tol {
let raan = normalize_angle(k.atan2(h));
let argp = 0.0;
let nu = normalize_angle(L - raan);
(raan, argp, nu)
} else {
let raan = normalize_angle(k.atan2(h));
let argp_plus_raan = g.atan2(f);
let argp = normalize_angle(argp_plus_raan - raan);
let nu = normalize_angle(L - argp_plus_raan);
(raan, argp, nu)
};
Ok(OrbitalElements::new(a, e, i, raan, argp, nu))
}
pub fn rv_to_equinoctial(
r: &Vector3,
v: &Vector3,
mu: f64,
tol: f64,
) -> PoliastroResult<EquinoctialElements> {
let coe = rv_to_coe(r, v, mu, tol)?;
Ok(coe_to_equinoctial(&coe))
}
pub fn equinoctial_to_rv(
elements: &EquinoctialElements,
mu: f64,
tol: f64,
) -> PoliastroResult<(Vector3, Vector3)> {
let coe = equinoctial_to_coe(elements, tol)?;
Ok(coe_to_rv(&coe, mu))
}
#[inline]
fn normalize_angle(angle: f64) -> f64 {
let mut normalized = angle % (2.0 * PI);
if normalized < 0.0 {
normalized += 2.0 * PI;
}
normalized
}
#[pymethods]
impl OrbitalElements {
#[new]
fn py_new(a: f64, e: f64, i: f64, raan: f64, argp: f64, nu: f64) -> Self {
Self::new(a, e, i, raan, argp, nu)
}
fn orbital_period(&self, mu: f64) -> PyResult<f64> {
self.period(mu).map_err(|e| e.into())
}
#[getter]
fn periapsis_distance(&self) -> f64 {
self.periapsis()
}
#[getter]
fn apoapsis_distance(&self) -> f64 {
self.apoapsis()
}
#[getter]
fn p(&self) -> f64 {
self.semi_latus_rectum()
}
fn h_magnitude(&self, mu: f64) -> f64 {
self.specific_angular_momentum_magnitude(mu)
}
fn __repr__(&self) -> String {
format!(
"OrbitalElements(a={:.3e} m, e={:.6}, i={:.6} rad, Ω={:.6} rad, ω={:.6} rad, ν={:.6} rad)",
self.a, self.e, self.i, self.raan, self.argp, self.nu
)
}
fn __str__(&self) -> String {
format!(
"a = {:.3} km\ne = {:.6}\ni = {:.3}°\nΩ = {:.3}°\nω = {:.3}°\nν = {:.3}°",
self.a / 1000.0,
self.e,
self.i.to_degrees(),
self.raan.to_degrees(),
self.argp.to_degrees(),
self.nu.to_degrees()
)
}
}
#[pymethods]
impl EquinoctialElements {
#[new]
fn py_new(p: f64, f: f64, g: f64, h: f64, k: f64, L: f64) -> Self {
Self::new(p, f, g, h, k, L)
}
#[getter]
fn eccentricity_value(&self) -> f64 {
self.eccentricity()
}
#[getter]
fn semi_major_axis_value(&self) -> PyResult<f64> {
self.semi_major_axis().map_err(|e| e.into())
}
#[getter]
fn inclination_value(&self) -> f64 {
self.inclination()
}
fn orbital_period(&self, mu: f64) -> PyResult<f64> {
self.period(mu).map_err(|e| e.into())
}
#[pyo3(signature = (tol=None))]
fn to_classical(&self, tol: Option<f64>) -> PyResult<OrbitalElements> {
equinoctial_to_coe(self, tol.unwrap_or(DEFAULT_TOL)).map_err(|e| e.into())
}
#[staticmethod]
fn from_classical(elements: &OrbitalElements) -> Self {
coe_to_equinoctial(elements)
}
fn __repr__(&self) -> String {
format!(
"EquinoctialElements(p={:.3e} m, f={:.6}, g={:.6}, h={:.6}, k={:.6}, L={:.6} rad)",
self.p, self.f, self.g, self.h, self.k, self.L
)
}
fn __str__(&self) -> String {
let e = self.eccentricity();
let i = self.inclination();
format!(
"p = {:.3} km\nf = {:.6}\ng = {:.6}\nh = {:.6}\nk = {:.6}\nL = {:.6} rad\n(derived: e = {:.6}, i = {:.3}°)",
self.p / 1000.0,
self.f,
self.g,
self.h,
self.k,
self.L,
e,
i.to_degrees()
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::constants::GM_EARTH;
use approx::assert_relative_eq;
#[test]
fn test_orbital_elements_period() {
let elements = OrbitalElements::new(6778e3, 0.0001, 0.0, 0.0, 0.0, 0.0);
let period = elements.period(GM_EARTH).unwrap();
let expected_period = 92.0 * 60.0; assert_relative_eq!(period, expected_period, epsilon = 100.0); }
#[test]
fn test_orbital_elements_periapsis_apoapsis() {
let a = 7000e3; let e = 0.1; let elements = OrbitalElements::new(a, e, 0.0, 0.0, 0.0, 0.0);
let r_p = elements.periapsis();
let r_a = elements.apoapsis();
assert_relative_eq!(r_p, a * (1.0 - e), epsilon = 1.0);
assert_relative_eq!(r_a, a * (1.0 + e), epsilon = 1.0);
assert_relative_eq!(r_p, 6300e3, epsilon = 1.0);
assert_relative_eq!(r_a, 7700e3, epsilon = 1.0);
}
#[test]
fn test_rv_to_coe_circular_equatorial() {
let r = Vector3::new(7000e3, 0.0, 0.0);
let v_mag = (GM_EARTH / 7000e3).sqrt(); let v = Vector3::new(0.0, v_mag, 0.0);
let elements = rv_to_coe(&r, &v, GM_EARTH, DEFAULT_TOL).unwrap();
assert_relative_eq!(elements.a, 7000e3, epsilon = 1.0);
assert!(elements.e < DEFAULT_TOL);
assert!(elements.i < DEFAULT_TOL);
}
#[test]
fn test_rv_to_coe_elliptical() {
let r = Vector3::new(7000e3, 0.0, 0.0);
let v = Vector3::new(0.0, 8000.0, 0.0);
let elements = rv_to_coe(&r, &v, GM_EARTH, DEFAULT_TOL).unwrap();
assert!(elements.e > 0.0);
assert!(elements.e < 1.0); }
#[test]
fn test_rv_to_coe_inclined() {
let r = Vector3::new(7000e3, 0.0, 0.0);
let v_mag = (GM_EARTH / 7000e3).sqrt();
let angle = PI / 4.0;
let v = Vector3::new(0.0, v_mag * angle.cos(), v_mag * angle.sin());
let elements = rv_to_coe(&r, &v, GM_EARTH, DEFAULT_TOL).unwrap();
assert_relative_eq!(elements.i, PI / 4.0, epsilon = 1e-6);
}
#[test]
fn test_coe_to_rv_circular_equatorial() {
let elements = OrbitalElements::new(7000e3, 0.0, 0.0, 0.0, 0.0, 0.0);
let (r, v) = coe_to_rv(&elements, GM_EARTH);
assert_relative_eq!(r.norm(), 7000e3, epsilon = 1.0);
assert_relative_eq!(r.x, 7000e3, epsilon = 1.0);
assert_relative_eq!(r.y, 0.0, epsilon = 1.0);
assert_relative_eq!(r.z, 0.0, epsilon = 1.0);
let v_expected = (GM_EARTH / 7000e3).sqrt();
assert_relative_eq!(v.norm(), v_expected, epsilon = 1.0);
}
#[test]
fn test_coe_to_rv_elliptical() {
let elements = OrbitalElements::new(8000e3, 0.1, 0.0, 0.0, 0.0, 0.0);
let (r, _v) = coe_to_rv(&elements, GM_EARTH);
let r_expected = 8000e3 * (1.0 - 0.1);
assert_relative_eq!(r.norm(), r_expected, epsilon = 1.0);
}
#[test]
fn test_roundtrip_conversion_circular() {
let r_orig = Vector3::new(7000e3, 0.0, 0.0);
let v_mag = (GM_EARTH / 7000e3).sqrt();
let v_orig = Vector3::new(0.0, v_mag, 0.0);
let elements = rv_to_coe(&r_orig, &v_orig, GM_EARTH, DEFAULT_TOL).unwrap();
let (r_new, v_new) = coe_to_rv(&elements, GM_EARTH);
assert_relative_eq!(r_new.x, r_orig.x, epsilon = 1.0);
assert_relative_eq!(r_new.y, r_orig.y, epsilon = 1.0);
assert_relative_eq!(r_new.z, r_orig.z, epsilon = 1.0);
assert_relative_eq!(v_new.x, v_orig.x, epsilon = 0.1);
assert_relative_eq!(v_new.y, v_orig.y, epsilon = 0.1);
assert_relative_eq!(v_new.z, v_orig.z, epsilon = 0.1);
}
#[test]
fn test_roundtrip_conversion_elliptical() {
let r_orig = Vector3::new(7000e3, 0.0, 0.0);
let v_orig = Vector3::new(0.0, 8000.0, 0.0);
let elements = rv_to_coe(&r_orig, &v_orig, GM_EARTH, DEFAULT_TOL).unwrap();
let (r_new, v_new) = coe_to_rv(&elements, GM_EARTH);
assert_relative_eq!(r_new.x, r_orig.x, epsilon = 10.0);
assert_relative_eq!(r_new.y, r_orig.y, epsilon = 10.0);
assert_relative_eq!(r_new.z, r_orig.z, epsilon = 10.0);
assert_relative_eq!(v_new.x, v_orig.x, epsilon = 1.0);
assert_relative_eq!(v_new.y, v_orig.y, epsilon = 1.0);
assert_relative_eq!(v_new.z, v_orig.z, epsilon = 1.0);
}
#[test]
fn test_roundtrip_conversion_inclined() {
let r_orig = Vector3::new(7000e3, 0.0, 0.0);
let v_mag = (GM_EARTH / 7000e3).sqrt();
let angle = PI / 3.0; let v_orig = Vector3::new(0.0, v_mag * angle.cos(), v_mag * angle.sin());
let elements = rv_to_coe(&r_orig, &v_orig, GM_EARTH, DEFAULT_TOL).unwrap();
let (r_new, v_new) = coe_to_rv(&elements, GM_EARTH);
assert_relative_eq!(r_new.norm(), r_orig.norm(), epsilon = 10.0);
assert_relative_eq!(v_new.norm(), v_orig.norm(), epsilon = 1.0);
let h_new = r_new.cross(&v_new);
let i_check = (h_new.z / h_new.norm()).acos();
assert_relative_eq!(i_check, angle, epsilon = 1e-6);
}
#[test]
fn test_semi_latus_rectum() {
let elements = OrbitalElements::new(7000e3, 0.1, 0.0, 0.0, 0.0, 0.0);
let p = elements.semi_latus_rectum();
let expected = 7000e3 * (1.0 - 0.1 * 0.1);
assert_relative_eq!(p, expected, epsilon = 1.0);
}
#[test]
fn test_rv_to_coe_rejects_zero_angular_momentum() {
let r = Vector3::new(7000e3, 0.0, 0.0);
let v = Vector3::new(1000.0, 0.0, 0.0);
let result = rv_to_coe(&r, &v, GM_EARTH, DEFAULT_TOL);
assert!(result.is_err());
}
#[test]
fn test_coe_to_equinoctial_circular_equatorial() {
let coe = OrbitalElements::new(7000e3, 0.0, 0.0, 0.0, 0.0, 0.0);
let eq = coe_to_equinoctial(&coe);
assert_relative_eq!(eq.f, 0.0, epsilon = DEFAULT_TOL);
assert_relative_eq!(eq.g, 0.0, epsilon = DEFAULT_TOL);
assert_relative_eq!(eq.h, 0.0, epsilon = DEFAULT_TOL);
assert_relative_eq!(eq.k, 0.0, epsilon = DEFAULT_TOL);
assert_relative_eq!(eq.p, 7000e3, epsilon = 1.0);
}
#[test]
fn test_coe_to_equinoctial_elliptical() {
let coe = OrbitalElements::new(8000e3, 0.1, 0.0, 0.0, 0.0, 0.0);
let eq = coe_to_equinoctial(&coe);
let p_expected = 8000e3 * (1.0 - 0.1 * 0.1);
assert_relative_eq!(eq.p, p_expected, epsilon = 1.0);
let e_recovered = (eq.f * eq.f + eq.g * eq.g).sqrt();
assert_relative_eq!(e_recovered, 0.1, epsilon = 1e-10);
assert_relative_eq!(eq.h, 0.0, epsilon = DEFAULT_TOL);
assert_relative_eq!(eq.k, 0.0, epsilon = DEFAULT_TOL);
}
#[test]
fn test_coe_to_equinoctial_inclined() {
let inc = PI / 6.0; let coe = OrbitalElements::new(7000e3, 0.0, inc, 0.0, 0.0, 0.0);
let eq = coe_to_equinoctial(&coe);
let i_recovered = 2.0 * (eq.h * eq.h + eq.k * eq.k).sqrt().atan();
assert_relative_eq!(i_recovered, inc, epsilon = 1e-10);
assert_relative_eq!(eq.f, 0.0, epsilon = DEFAULT_TOL);
assert_relative_eq!(eq.g, 0.0, epsilon = DEFAULT_TOL);
}
#[test]
fn test_equinoctial_to_coe_circular_equatorial() {
let eq = EquinoctialElements::new(7000e3, 0.0, 0.0, 0.0, 0.0, 0.5);
let coe = equinoctial_to_coe(&eq, DEFAULT_TOL).unwrap();
assert_relative_eq!(coe.a, 7000e3, epsilon = 1.0);
assert!(coe.e < DEFAULT_TOL);
assert!(coe.i < DEFAULT_TOL);
}
#[test]
fn test_equinoctial_to_coe_elliptical() {
let eq = EquinoctialElements::new(7.92e6, 0.1, 0.0, 0.0, 0.0, 0.5);
let coe = equinoctial_to_coe(&eq, DEFAULT_TOL).unwrap();
assert_relative_eq!(coe.e, 0.1, epsilon = 1e-10);
let a_expected = 7.92e6 / (1.0 - 0.1 * 0.1);
assert_relative_eq!(coe.a, a_expected, epsilon = 1.0);
}
#[test]
fn test_roundtrip_coe_equinoctial_circular() {
let coe_orig = OrbitalElements::new(7000e3, 0.0, 0.0, 0.0, 0.0, 0.5);
let eq = coe_to_equinoctial(&coe_orig);
let coe_new = equinoctial_to_coe(&eq, DEFAULT_TOL).unwrap();
assert_relative_eq!(coe_new.a, coe_orig.a, epsilon = 1.0);
assert_relative_eq!(coe_new.e, coe_orig.e, epsilon = 1e-10);
assert_relative_eq!(coe_new.i, coe_orig.i, epsilon = 1e-10);
assert_relative_eq!(coe_new.nu, coe_orig.nu, epsilon = 1e-8);
}
#[test]
fn test_roundtrip_coe_equinoctial_elliptical() {
let coe_orig = OrbitalElements::new(8000e3, 0.1, PI/4.0, 0.5, 0.3, 0.7);
let eq = coe_to_equinoctial(&coe_orig);
let coe_new = equinoctial_to_coe(&eq, DEFAULT_TOL).unwrap();
assert_relative_eq!(coe_new.a, coe_orig.a, epsilon = 1.0);
assert_relative_eq!(coe_new.e, coe_orig.e, epsilon = 1e-10);
assert_relative_eq!(coe_new.i, coe_orig.i, epsilon = 1e-10);
assert_relative_eq!(coe_new.raan, coe_orig.raan, epsilon = 1e-8);
assert_relative_eq!(coe_new.argp, coe_orig.argp, epsilon = 1e-8);
assert_relative_eq!(coe_new.nu, coe_orig.nu, epsilon = 1e-8);
}
#[test]
fn test_rv_to_equinoctial_circular() {
let r = Vector3::new(7000e3, 0.0, 0.0);
let v_mag = (GM_EARTH / 7000e3).sqrt();
let v = Vector3::new(0.0, v_mag, 0.0);
let eq = rv_to_equinoctial(&r, &v, GM_EARTH, DEFAULT_TOL).unwrap();
let e = eq.eccentricity();
assert!(e < DEFAULT_TOL);
let a = eq.semi_major_axis().unwrap();
assert_relative_eq!(a, 7000e3, epsilon = 1.0);
}
#[test]
fn test_equinoctial_to_rv_circular() {
let eq = EquinoctialElements::new(7000e3, 0.0, 0.0, 0.0, 0.0, 0.0);
let (r, v) = equinoctial_to_rv(&eq, GM_EARTH, DEFAULT_TOL).unwrap();
assert_relative_eq!(r.norm(), 7000e3, epsilon = 1.0);
let v_expected = (GM_EARTH / 7000e3).sqrt();
assert_relative_eq!(v.norm(), v_expected, epsilon = 1.0);
}
#[test]
fn test_roundtrip_rv_equinoctial() {
let r_orig = Vector3::new(7000e3, 0.0, 0.0);
let v_orig = Vector3::new(0.0, 8000.0, 0.0);
let eq = rv_to_equinoctial(&r_orig, &v_orig, GM_EARTH, DEFAULT_TOL).unwrap();
let (r_new, v_new) = equinoctial_to_rv(&eq, GM_EARTH, DEFAULT_TOL).unwrap();
assert_relative_eq!(r_new.x, r_orig.x, epsilon = 10.0);
assert_relative_eq!(r_new.y, r_orig.y, epsilon = 10.0);
assert_relative_eq!(r_new.z, r_orig.z, epsilon = 10.0);
assert_relative_eq!(v_new.x, v_orig.x, epsilon = 1.0);
assert_relative_eq!(v_new.y, v_orig.y, epsilon = 1.0);
assert_relative_eq!(v_new.z, v_orig.z, epsilon = 1.0);
}
#[test]
fn test_equinoctial_elements_eccentricity() {
let eq = EquinoctialElements::new(7000e3, 0.06, 0.08, 0.0, 0.0, 0.0);
let e = eq.eccentricity();
assert_relative_eq!(e, 0.1, epsilon = 1e-10);
}
#[test]
fn test_equinoctial_elements_inclination() {
let tan_half_i = (PI / 6.0).tan(); let eq = EquinoctialElements::new(7000e3, 0.0, 0.0, tan_half_i, 0.0, 0.0);
let i = eq.inclination();
assert_relative_eq!(i, PI / 3.0, epsilon = 1e-10); }
#[test]
fn test_equinoctial_elements_period() {
let eq = EquinoctialElements::new(7000e3, 0.0, 0.0, 0.0, 0.0, 0.0);
let period = eq.period(GM_EARTH).unwrap();
let expected_period = 97.0 * 60.0;
assert_relative_eq!(period, expected_period, epsilon = 100.0);
}
#[test]
fn test_equinoctial_singularity_free_near_circular() {
let coe = OrbitalElements::new(7000e3, 1e-10, PI/4.0, 0.5, 0.0, 0.3);
let eq = coe_to_equinoctial(&coe);
let coe_recovered = equinoctial_to_coe(&eq, DEFAULT_TOL).unwrap();
assert_relative_eq!(coe_recovered.e, 1e-10, epsilon = 1e-12);
assert_relative_eq!(coe_recovered.a, 7000e3, epsilon = 1.0);
}
#[test]
fn test_equinoctial_singularity_free_near_equatorial() {
let coe = OrbitalElements::new(7000e3, 0.1, 1e-10, 0.0, 0.3, 0.5);
let eq = coe_to_equinoctial(&coe);
let coe_recovered = equinoctial_to_coe(&eq, DEFAULT_TOL).unwrap();
assert_relative_eq!(coe_recovered.e, 0.1, epsilon = 1e-10);
assert_relative_eq!(coe_recovered.i, 1e-10, epsilon = 1e-12);
}
}