#[cfg(all(test, feature = "python-tests"))]
mod python_tests;
use std::f64::consts::PI;
use nalgebra::Vector3;
use crate::constants::{AU_KM, DAY_S};
const TAU: f64 = 2.0 * PI;
#[derive(Debug, Clone)]
pub struct OsculatingElements {
h_vec: Vector3<f64>,
e_vec: Vector3<f64>,
n_vec: Vector3<f64>,
pos_km: Vector3<f64>,
vel_km_s: Vector3<f64>,
mu: f64,
}
impl OsculatingElements {
pub fn new(pos_km: Vector3<f64>, vel_km_s: Vector3<f64>, mu_km3_s2: f64) -> Self {
let h_vec = pos_km.cross(&vel_km_s);
let e_vec = eccentricity_vector(&pos_km, &vel_km_s, mu_km3_s2);
let n_vec = node_vector(&h_vec);
OsculatingElements {
h_vec,
e_vec,
n_vec,
pos_km,
vel_km_s,
mu: mu_km3_s2,
}
}
pub fn from_au(pos_au: &Vector3<f64>, vel_au_day: &Vector3<f64>, mu_km3_s2: f64) -> Self {
let pos_km = pos_au * AU_KM;
let vel_km_s = vel_au_day * AU_KM / DAY_S;
Self::new(pos_km, vel_km_s, mu_km3_s2)
}
pub fn eccentricity(&self) -> f64 {
self.e_vec.norm()
}
pub fn inclination_rad(&self) -> f64 {
let k = Vector3::new(0.0, 0.0, 1.0);
angle_between(&self.h_vec, &k)
}
pub fn inclination_deg(&self) -> f64 {
self.inclination_rad().to_degrees()
}
pub fn longitude_of_ascending_node_rad(&self) -> f64 {
let i = self.inclination_rad();
if i.abs() < 1e-15 {
0.0
} else {
self.h_vec.x.atan2(-self.h_vec.y).rem_euclid(TAU)
}
}
pub fn longitude_of_ascending_node_deg(&self) -> f64 {
self.longitude_of_ascending_node_rad().to_degrees()
}
pub fn argument_of_periapsis_rad(&self) -> f64 {
let e = self.eccentricity();
let n_len = self.n_vec.norm();
if e < 1e-15 {
0.0
} else if n_len < 1e-15 {
let angle = self.e_vec.y.atan2(self.e_vec.x).rem_euclid(TAU);
let h_z = self.pos_km.cross(&self.vel_km_s).z;
if h_z >= 0.0 {
angle
} else {
(-angle).rem_euclid(TAU)
}
} else {
let angle = angle_between(&self.n_vec, &self.e_vec);
if self.e_vec.z > 0.0 {
angle
} else {
(-angle).rem_euclid(TAU)
}
}
}
pub fn argument_of_periapsis_deg(&self) -> f64 {
self.argument_of_periapsis_rad().to_degrees()
}
pub fn true_anomaly_rad(&self) -> f64 {
let e = self.eccentricity();
let n_len = self.n_vec.norm();
let v = if e > 1e-15 {
let angle = angle_between(&self.e_vec, &self.pos_km);
if self.pos_km.dot(&self.vel_km_s) > 0.0 {
angle
} else {
(-angle).rem_euclid(TAU)
}
} else if n_len < 1e-15 {
let r = self.pos_km.norm();
let angle = (self.pos_km.x / r).clamp(-1.0, 1.0).acos();
if self.vel_km_s.x < 0.0 {
angle
} else {
(-angle).rem_euclid(TAU)
}
} else {
let angle = angle_between(&self.n_vec, &self.pos_km);
if self.pos_km.z >= 0.0 {
angle
} else {
(-angle).rem_euclid(TAU)
}
};
if e > (1.0 - 1e-15) {
normpi(v)
} else {
v
}
}
pub fn true_anomaly_deg(&self) -> f64 {
self.true_anomaly_rad().to_degrees()
}
pub fn eccentric_anomaly_rad(&self) -> f64 {
let e = self.eccentricity();
let v = self.true_anomaly_rad();
if e < 1.0 {
2.0 * (((1.0 - e) / (1.0 + e)).sqrt() * (v / 2.0).tan()).atan()
} else if e > 1.0 {
let val = (v / 2.0).tan() / ((e + 1.0) / (e - 1.0)).sqrt();
normpi(2.0 * val.atanh())
} else {
0.0 }
}
pub fn mean_anomaly_rad(&self) -> f64 {
let e = self.eccentricity();
let ea = self.eccentric_anomaly_rad();
if e < 1.0 {
(ea - e * ea.sin()).rem_euclid(TAU)
} else if e > 1.0 {
normpi(e * ea.sinh() - ea)
} else {
0.0
}
}
pub fn mean_anomaly_deg(&self) -> f64 {
self.mean_anomaly_rad().to_degrees()
}
pub fn semi_latus_rectum_km(&self) -> f64 {
let h = self.h_vec.norm();
h * h / self.mu
}
pub fn semi_latus_rectum_au(&self) -> f64 {
self.semi_latus_rectum_km() / AU_KM
}
pub fn semi_major_axis_km(&self) -> f64 {
let p = self.semi_latus_rectum_km();
let e = self.eccentricity();
if (e - 1.0).abs() > 1e-15 {
p / (1.0 - e * e)
} else {
f64::INFINITY
}
}
pub fn semi_major_axis_au(&self) -> f64 {
self.semi_major_axis_km() / AU_KM
}
pub fn semi_minor_axis_km(&self) -> f64 {
let p = self.semi_latus_rectum_km();
let e = self.eccentricity();
if e < 1.0 {
p / (1.0 - e * e).sqrt()
} else if e > 1.0 {
p * (e * e - 1.0).sqrt() / (1.0 - e * e)
} else {
0.0
}
}
pub fn periapsis_distance_km(&self) -> f64 {
let p = self.semi_latus_rectum_km();
let e = self.eccentricity();
if (e - 1.0).abs() > 1e-15 {
p * (1.0 - e) / (1.0 - e * e)
} else {
p / 2.0
}
}
pub fn periapsis_distance_au(&self) -> f64 {
self.periapsis_distance_km() / AU_KM
}
pub fn apoapsis_distance_km(&self) -> f64 {
let p = self.semi_latus_rectum_km();
let e = self.eccentricity();
if e < (1.0 - 1e-15) {
p * (1.0 + e) / (1.0 - e * e)
} else {
f64::INFINITY
}
}
pub fn apoapsis_distance_au(&self) -> f64 {
self.apoapsis_distance_km() / AU_KM
}
pub fn period_seconds(&self) -> f64 {
let a = self.semi_major_axis_km();
if a > 0.0 && a.is_finite() {
TAU * (a.powi(3) / self.mu).sqrt()
} else {
f64::INFINITY
}
}
pub fn period_days(&self) -> f64 {
self.period_seconds() / DAY_S
}
pub fn mean_motion_rad_per_sec(&self) -> f64 {
let a = self.semi_major_axis_km();
(self.mu / a.abs().powi(3)).sqrt()
}
pub fn mean_motion_rad_per_day(&self) -> f64 {
self.mean_motion_rad_per_sec() * DAY_S
}
pub fn argument_of_latitude_rad(&self) -> f64 {
(self.argument_of_periapsis_rad() + self.true_anomaly_rad()).rem_euclid(TAU)
}
pub fn longitude_of_periapsis_rad(&self) -> f64 {
(self.longitude_of_ascending_node_rad() + self.argument_of_periapsis_rad()).rem_euclid(TAU)
}
pub fn true_longitude_rad(&self) -> f64 {
(self.longitude_of_ascending_node_rad()
+ self.argument_of_periapsis_rad()
+ self.true_anomaly_rad())
.rem_euclid(TAU)
}
pub fn mean_longitude_rad(&self) -> f64 {
(self.longitude_of_ascending_node_rad()
+ self.argument_of_periapsis_rad()
+ self.mean_anomaly_rad())
.rem_euclid(TAU)
}
}
impl std::fmt::Display for OsculatingElements {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"OsculatingElements(a={:.4} AU, e={:.6}, i={:.2}°)",
self.semi_major_axis_au(),
self.eccentricity(),
self.inclination_deg(),
)
}
}
fn normpi(x: f64) -> f64 {
(x + PI).rem_euclid(TAU) - PI
}
fn eccentricity_vector(pos: &Vector3<f64>, vel: &Vector3<f64>, mu: f64) -> Vector3<f64> {
let r = pos.norm();
let v_sq = vel.norm_squared();
((v_sq - mu / r) * pos - pos.dot(vel) * vel) / mu
}
fn node_vector(h: &Vector3<f64>) -> Vector3<f64> {
let n = Vector3::new(-h.y, h.x, 0.0);
let len = n.norm();
if len > 0.0 {
n / len
} else {
n
}
}
fn angle_between(a: &Vector3<f64>, b: &Vector3<f64>) -> f64 {
let cos_angle = a.dot(b) / (a.norm() * b.norm());
cos_angle.clamp(-1.0, 1.0).acos()
}
pub fn osculating_elements_of(
pos_au: &Vector3<f64>,
vel_au_day: &Vector3<f64>,
mu_km3_s2: f64,
) -> OsculatingElements {
OsculatingElements::from_au(pos_au, vel_au_day, mu_km3_s2)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::constants::GM_SUN;
use approx::assert_relative_eq;
#[test]
fn test_circular_orbit() {
let r_km = AU_KM;
let v_km_s = (GM_SUN / r_km).sqrt();
let pos = Vector3::new(r_km, 0.0, 0.0);
let vel = Vector3::new(0.0, v_km_s, 0.0);
let elem = OsculatingElements::new(pos, vel, GM_SUN);
assert_relative_eq!(elem.eccentricity(), 0.0, epsilon = 1e-10);
assert_relative_eq!(elem.semi_major_axis_km(), r_km, epsilon = 1.0);
assert_relative_eq!(elem.inclination_rad(), 0.0, epsilon = 1e-10);
assert_relative_eq!(elem.periapsis_distance_km(), r_km, epsilon = 1.0);
}
#[test]
fn test_elliptic_orbit() {
let a = AU_KM;
let e = 0.5;
let r_periapsis = a * (1.0 - e);
let v_periapsis = ((GM_SUN / a) * (1.0 + e) / (1.0 - e)).sqrt();
let pos = Vector3::new(r_periapsis, 0.0, 0.0);
let vel = Vector3::new(0.0, v_periapsis, 0.0);
let elem = OsculatingElements::new(pos, vel, GM_SUN);
assert_relative_eq!(elem.eccentricity(), e, epsilon = 1e-10);
assert_relative_eq!(elem.semi_major_axis_km(), a, epsilon = 1.0);
assert_relative_eq!(elem.periapsis_distance_km(), r_periapsis, epsilon = 1.0);
assert_relative_eq!(elem.apoapsis_distance_km(), a * (1.0 + e), epsilon = 1.0);
}
#[test]
fn test_inclined_orbit() {
let r_km = AU_KM;
let v_km_s = (GM_SUN / r_km).sqrt();
let angle = 45.0_f64.to_radians();
let pos = Vector3::new(r_km, 0.0, 0.0);
let vel = Vector3::new(0.0, v_km_s * angle.cos(), v_km_s * angle.sin());
let elem = OsculatingElements::new(pos, vel, GM_SUN);
assert_relative_eq!(elem.inclination_deg(), 45.0, epsilon = 1e-8);
assert_relative_eq!(elem.eccentricity(), 0.0, epsilon = 1e-10);
}
#[test]
fn test_period() {
let r_km = AU_KM;
let v_km_s = (GM_SUN / r_km).sqrt();
let pos = Vector3::new(r_km, 0.0, 0.0);
let vel = Vector3::new(0.0, v_km_s, 0.0);
let elem = OsculatingElements::new(pos, vel, GM_SUN);
let period_days = elem.period_days();
assert_relative_eq!(period_days, 365.256, epsilon = 0.01);
}
#[test]
fn test_from_au() {
let pos_au = Vector3::new(1.0, 0.0, 0.0);
let vel_au_day = Vector3::new(0.0, (GM_SUN / AU_KM).sqrt() * DAY_S / AU_KM, 0.0);
let elem = OsculatingElements::from_au(&pos_au, &vel_au_day, GM_SUN);
assert_relative_eq!(elem.semi_major_axis_au(), 1.0, epsilon = 1e-6);
assert_relative_eq!(elem.eccentricity(), 0.0, epsilon = 1e-10);
}
#[test]
fn test_mean_anomaly_at_periapsis() {
let a = AU_KM;
let e = 0.5;
let r_periapsis = a * (1.0 - e);
let v_periapsis = ((GM_SUN / a) * (1.0 + e) / (1.0 - e)).sqrt();
let pos = Vector3::new(r_periapsis, 0.0, 0.0);
let vel = Vector3::new(0.0, v_periapsis, 0.0);
let elem = OsculatingElements::new(pos, vel, GM_SUN);
assert_relative_eq!(elem.true_anomaly_rad(), 0.0, epsilon = 1e-10);
assert_relative_eq!(elem.eccentric_anomaly_rad(), 0.0, epsilon = 1e-10);
assert_relative_eq!(elem.mean_anomaly_rad(), 0.0, epsilon = 1e-10);
}
#[test]
fn test_display() {
let pos = Vector3::new(AU_KM, 0.0, 0.0);
let vel = Vector3::new(0.0, (GM_SUN / AU_KM).sqrt(), 0.0);
let elem = OsculatingElements::new(pos, vel, GM_SUN);
let s = format!("{elem}");
assert!(s.contains("OsculatingElements"));
assert!(s.contains("AU"));
}
#[test]
fn test_normpi() {
assert_relative_eq!(normpi(0.0), 0.0, epsilon = 1e-14);
assert_relative_eq!(normpi(PI), -PI, epsilon = 1e-10);
assert_relative_eq!(normpi(3.0 * PI), -PI, epsilon = 1e-10);
}
#[test]
fn test_hyperbolic_orbit() {
let r_km = AU_KM;
let v_escape = (2.0 * GM_SUN / r_km).sqrt();
let v_hyp = v_escape * 1.5;
let pos = Vector3::new(r_km, 0.0, 0.0);
let vel = Vector3::new(0.0, v_hyp, 0.0);
let elem = OsculatingElements::new(pos, vel, GM_SUN);
assert!(
elem.eccentricity() > 1.0,
"e should be > 1, got {}",
elem.eccentricity()
);
assert!(
elem.semi_major_axis_km() < 0.0,
"a should be negative for hyperbolic"
);
assert!(
elem.apoapsis_distance_km().is_infinite(),
"Q should be infinite"
);
assert!(
elem.period_days().is_infinite(),
"Period should be infinite"
);
}
#[test]
fn test_longitude_quantities() {
let a = AU_KM;
let e = 0.3;
let r = a * (1.0 - e);
let v = ((GM_SUN / a) * (1.0 + e) / (1.0 - e)).sqrt();
let pos = Vector3::new(r, 0.0, 0.0);
let vel = Vector3::new(0.0, v, 0.0);
let elem = OsculatingElements::new(pos, vel, GM_SUN);
assert_relative_eq!(elem.true_anomaly_rad(), 0.0, epsilon = 1e-10);
assert_relative_eq!(elem.true_longitude_rad(), 0.0, epsilon = 1e-10);
assert_relative_eq!(elem.mean_longitude_rad(), 0.0, epsilon = 1e-10);
}
}