use serde::{Deserialize, Serialize};
use tracing::instrument;
use crate::error::{FalakError, Result};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct OrbitalElements {
pub semi_major_axis: f64,
pub eccentricity: f64,
pub inclination: f64,
pub raan: f64,
pub argument_of_periapsis: f64,
pub true_anomaly: f64,
}
impl OrbitalElements {
#[must_use = "returns the validated orbital elements"]
#[instrument(level = "trace")]
pub fn new(
semi_major_axis: f64,
eccentricity: f64,
inclination: f64,
raan: f64,
argument_of_periapsis: f64,
true_anomaly: f64,
) -> Result<Self> {
if eccentricity < 0.0 {
return Err(FalakError::InvalidParameter(
format!("eccentricity must be non-negative, got {eccentricity}").into(),
));
}
if eccentricity < 1.0 && semi_major_axis <= 0.0 {
return Err(FalakError::InvalidParameter(
format!(
"semi-major axis must be positive for elliptical orbits, got {semi_major_axis}"
)
.into(),
));
}
if eccentricity > 1.0 && semi_major_axis >= 0.0 {
return Err(FalakError::InvalidParameter(
format!(
"semi-major axis must be negative for hyperbolic orbits, got {semi_major_axis}"
)
.into(),
));
}
if eccentricity == 1.0 && semi_major_axis <= 0.0 {
return Err(FalakError::InvalidParameter(
format!(
"semi-latus rectum (stored as semi_major_axis) must be positive for parabolic orbits, got {semi_major_axis}"
)
.into(),
));
}
if !(0.0..=std::f64::consts::PI).contains(&inclination) {
return Err(FalakError::InvalidParameter(
format!("inclination must be in [0, pi], got {inclination}").into(),
));
}
Ok(Self {
semi_major_axis,
eccentricity,
inclination,
raan,
argument_of_periapsis,
true_anomaly,
})
}
#[must_use]
#[inline]
pub fn periapsis(&self) -> f64 {
if (self.eccentricity - 1.0).abs() < 1e-10 {
self.semi_major_axis / 2.0
} else {
self.semi_major_axis.abs() * (1.0 - self.eccentricity).abs()
}
}
#[must_use]
#[inline]
pub fn apoapsis(&self) -> f64 {
if self.eccentricity >= 1.0 {
f64::INFINITY
} else {
self.semi_major_axis * (1.0 + self.eccentricity)
}
}
#[must_use]
#[inline]
pub fn semi_latus_rectum(&self) -> f64 {
self.semi_major_axis.abs() * (1.0 - self.eccentricity * self.eccentricity).abs()
}
#[must_use]
#[inline]
pub fn is_elliptical(&self) -> bool {
self.eccentricity < 1.0
}
#[must_use]
#[inline]
pub fn is_circular(&self) -> bool {
self.eccentricity < 1e-10
}
#[must_use]
#[inline]
pub fn is_parabolic(&self) -> bool {
(self.eccentricity - 1.0).abs() < 1e-10
}
#[must_use]
#[inline]
pub fn is_hyperbolic(&self) -> bool {
self.eccentricity > 1.0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn valid_elliptical() {
let orbit = OrbitalElements::new(7000.0e3, 0.01, 0.9, 1.0, 0.5, 0.0);
assert!(orbit.is_ok());
assert!(orbit.unwrap().is_elliptical());
}
#[test]
fn valid_hyperbolic() {
let orbit = OrbitalElements::new(-10000.0e3, 1.5, 0.5, 0.0, 0.0, 0.5);
assert!(orbit.is_ok());
assert!(orbit.unwrap().is_hyperbolic());
}
#[test]
fn valid_parabolic() {
let orbit = OrbitalElements::new(7000.0e3, 1.0, 0.0, 0.0, 0.0, 0.0);
assert!(orbit.is_ok());
assert!(orbit.unwrap().is_parabolic());
}
#[test]
fn negative_sma_rejected_elliptical() {
let orbit = OrbitalElements::new(-1.0, 0.5, 0.0, 0.0, 0.0, 0.0);
assert!(matches!(orbit, Err(FalakError::InvalidParameter(_))));
}
#[test]
fn positive_sma_rejected_hyperbolic() {
let orbit = OrbitalElements::new(1000.0, 1.5, 0.0, 0.0, 0.0, 0.0);
assert!(matches!(orbit, Err(FalakError::InvalidParameter(_))));
}
#[test]
fn negative_eccentricity_rejected() {
let orbit = OrbitalElements::new(7000.0e3, -0.1, 0.0, 0.0, 0.0, 0.0);
assert!(matches!(orbit, Err(FalakError::InvalidParameter(_))));
}
#[test]
fn periapsis_apoapsis_elliptical() {
let orbit = OrbitalElements::new(10000.0, 0.5, 0.0, 0.0, 0.0, 0.0).unwrap();
assert!((orbit.periapsis() - 5000.0).abs() < 1e-10);
assert!((orbit.apoapsis() - 15000.0).abs() < 1e-10);
}
#[test]
fn apoapsis_hyperbolic_infinite() {
let orbit = OrbitalElements::new(-10000.0, 1.5, 0.0, 0.0, 0.0, 0.0).unwrap();
assert!(orbit.apoapsis().is_infinite());
assert!(orbit.periapsis() > 0.0);
}
#[test]
fn semi_latus_rectum() {
let orbit = OrbitalElements::new(10000.0, 0.5, 0.0, 0.0, 0.0, 0.0).unwrap();
assert!((orbit.semi_latus_rectum() - 7500.0).abs() < 1e-10);
}
#[test]
fn periapsis_parabolic() {
let orbit = OrbitalElements::new(14000.0, 1.0, 0.0, 0.0, 0.0, 0.0).unwrap();
assert!(
(orbit.periapsis() - 7000.0).abs() < 1e-10,
"parabolic periapsis: {}",
orbit.periapsis()
);
}
#[test]
fn periapsis_hyperbolic() {
let orbit = OrbitalElements::new(-10000.0, 1.5, 0.0, 0.0, 0.0, 0.0).unwrap();
assert!(
(orbit.periapsis() - 5000.0).abs() < 1e-10,
"hyperbolic periapsis: {}",
orbit.periapsis()
);
}
#[test]
fn orbit_type_queries() {
let circ = OrbitalElements::new(7000.0, 0.0, 0.0, 0.0, 0.0, 0.0).unwrap();
assert!(circ.is_circular());
assert!(circ.is_elliptical());
assert!(!circ.is_hyperbolic());
assert!(!circ.is_parabolic());
}
}