use crate::qtty::angular_rate::AngularRate;
use crate::qtty::*;
use crate::time::JulianDate;
use affn::conic::{
ClassifiedSemiMajorAxisParam, ConicOrientation, ConicValidationError, Elliptic, OrientedConic,
PeriapsisParam, SemiMajorAxisParam, TypedSemiMajorAxisParam,
};
use affn::frames::EclipticMeanJ2000;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
pub use affn::conic::ConicKind;
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum ConicError {
InvalidEccentricity,
InvalidSemiMajorAxis,
InvalidPeriapsisDistance,
ParabolicSemiMajorAxis,
InvalidOrientation,
ParabolicUnsupported,
HyperbolicNotSupported,
InvalidEpoch,
InvalidMeanAnomaly,
InvalidMeanMotion,
HyperbolicSolverFailed,
OutOfRange {
field: &'static str,
value: f64,
},
}
impl std::fmt::Display for ConicError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidEccentricity => write!(f, "invalid eccentricity"),
Self::InvalidSemiMajorAxis => write!(f, "invalid semi-major axis"),
Self::InvalidPeriapsisDistance => write!(f, "invalid periapsis distance"),
Self::ParabolicSemiMajorAxis => {
write!(
f,
"semi-major axis is undefined for parabolic conics (e == 1)"
)
}
Self::InvalidOrientation => write!(f, "orientation angles must be finite"),
Self::ParabolicUnsupported => write!(f, "parabolic orbits are not supported"),
Self::HyperbolicNotSupported => {
write!(
f,
"hyperbolic eccentricity (e >= 1) is not supported by this orbit type; \
use ConicOrbit instead"
)
}
Self::InvalidEpoch => write!(f, "epoch must be finite"),
Self::InvalidMeanAnomaly => write!(f, "mean anomaly at epoch must be finite"),
Self::InvalidMeanMotion => write!(f, "mean motion must be finite and positive"),
Self::HyperbolicSolverFailed => {
write!(f, "hyperbolic anomaly solver failed to converge")
}
Self::OutOfRange { field, value } => {
write!(f, "parameter '{field}' has out-of-range value {value}")
}
}
}
}
impl std::error::Error for ConicError {}
pub(crate) fn map_validation_error(error: ConicValidationError) -> ConicError {
match error {
ConicValidationError::InvalidEccentricity => ConicError::InvalidEccentricity,
ConicValidationError::InvalidSemiMajorAxis => ConicError::InvalidSemiMajorAxis,
ConicValidationError::InvalidPeriapsisDistance => ConicError::InvalidPeriapsisDistance,
ConicValidationError::ParabolicSemiMajorAxis => ConicError::ParabolicSemiMajorAxis,
ConicValidationError::InvalidOrientation => ConicError::InvalidOrientation,
ConicValidationError::OutOfRange { .. } => ConicError::InvalidOrientation,
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct ConicOrbit {
geometry: OrientedConic<PeriapsisParam<AstronomicalUnit>, EclipticMeanJ2000>,
pub mean_anomaly_at_epoch: Degrees,
pub epoch: JulianDate,
}
impl ConicOrbit {
pub fn try_new(
periapsis_distance: AstronomicalUnits,
eccentricity: f64,
inclination: Degrees,
longitude_of_ascending_node: Degrees,
argument_of_periapsis: Degrees,
mean_anomaly_at_epoch: Degrees,
epoch: JulianDate,
) -> Result<Self, ConicError> {
let shape = PeriapsisParam::try_new(periapsis_distance, eccentricity)
.map_err(map_validation_error)?;
if eccentricity == 1.0 {
return Err(ConicError::ParabolicUnsupported);
}
let orientation = ConicOrientation::try_new(
inclination,
longitude_of_ascending_node,
argument_of_periapsis,
)
.map_err(map_validation_error)?;
if !mean_anomaly_at_epoch.is_finite() {
return Err(ConicError::InvalidMeanAnomaly);
}
if !epoch.jd_value().is_finite() {
return Err(ConicError::InvalidEpoch);
}
Ok(Self {
geometry: OrientedConic::new(shape, orientation),
mean_anomaly_at_epoch,
epoch,
})
}
pub const fn new_unchecked(
periapsis_distance: AstronomicalUnits,
eccentricity: f64,
inclination: Degrees,
longitude_of_ascending_node: Degrees,
argument_of_periapsis: Degrees,
mean_anomaly_at_epoch: Degrees,
epoch: JulianDate,
) -> Self {
Self {
geometry: OrientedConic::new(
PeriapsisParam::new_unchecked(periapsis_distance, eccentricity),
ConicOrientation::new(
inclination,
longitude_of_ascending_node,
argument_of_periapsis,
),
),
mean_anomaly_at_epoch,
epoch,
}
}
#[inline]
pub fn geometry(&self) -> &OrientedConic<PeriapsisParam<AstronomicalUnit>, EclipticMeanJ2000> {
&self.geometry
}
pub fn kind(&self) -> ConicKind {
self.geometry.kind()
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct MeanMotionOrbit {
geometry: OrientedConic<TypedSemiMajorAxisParam<AstronomicalUnit, Elliptic>, EclipticMeanJ2000>,
pub mean_motion: AngularRate<Degree, Day>,
pub epoch: JulianDate,
}
impl MeanMotionOrbit {
pub fn try_new(
semi_major_axis: AstronomicalUnits,
eccentricity: f64,
inclination: Degrees,
longitude_of_ascending_node: Degrees,
argument_of_periapsis: Degrees,
mean_motion: AngularRate<Degree, Day>,
epoch: JulianDate,
) -> Result<Self, ConicError> {
let sma = SemiMajorAxisParam::try_new(semi_major_axis, eccentricity)
.map_err(map_validation_error)?;
let typed = match sma.classify() {
ClassifiedSemiMajorAxisParam::Elliptic(t) => t,
ClassifiedSemiMajorAxisParam::Hyperbolic(_) => {
return Err(ConicError::HyperbolicNotSupported);
}
};
let orientation = ConicOrientation::try_new(
inclination,
longitude_of_ascending_node,
argument_of_periapsis,
)
.map_err(map_validation_error)?;
if !mean_motion.value().is_finite() || mean_motion.value() <= 0.0 {
return Err(ConicError::InvalidMeanMotion);
}
if !epoch.jd_value().is_finite() {
return Err(ConicError::InvalidEpoch);
}
Ok(Self {
geometry: OrientedConic::new(typed, orientation),
mean_motion,
epoch,
})
}
pub const fn new_unchecked(
semi_major_axis: AstronomicalUnits,
eccentricity: f64,
inclination: Degrees,
longitude_of_ascending_node: Degrees,
argument_of_periapsis: Degrees,
mean_motion: AngularRate<Degree, Day>,
epoch: JulianDate,
) -> Self {
Self {
geometry: OrientedConic::new(
TypedSemiMajorAxisParam::new_unchecked(SemiMajorAxisParam::new_unchecked(
semi_major_axis,
eccentricity,
)),
ConicOrientation::new(
inclination,
longitude_of_ascending_node,
argument_of_periapsis,
),
),
mean_motion,
epoch,
}
}
#[inline]
pub fn geometry(
&self,
) -> &OrientedConic<TypedSemiMajorAxisParam<AstronomicalUnit, Elliptic>, EclipticMeanJ2000>
{
&self.geometry
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn classify_elliptic() {
let orbit = ConicOrbit::try_new(
1.0 * AU,
0.5,
Degrees::new(0.0),
Degrees::new(0.0),
Degrees::new(0.0),
Degrees::new(0.0),
JulianDate::J2000,
)
.unwrap();
assert_eq!(orbit.kind(), ConicKind::Elliptic);
}
#[test]
fn classify_hyperbolic() {
let orbit = ConicOrbit::try_new(
1.0 * AU,
1.5,
Degrees::new(0.0),
Degrees::new(0.0),
Degrees::new(0.0),
Degrees::new(0.0),
JulianDate::J2000,
)
.unwrap();
assert_eq!(orbit.kind(), ConicKind::Hyperbolic);
}
#[test]
fn negative_eccentricity_is_invalid() {
assert_eq!(
ConicOrbit::try_new(
1.0 * AU,
-0.1,
Degrees::new(0.0),
Degrees::new(0.0),
Degrees::new(0.0),
Degrees::new(0.0),
JulianDate::J2000,
),
Err(ConicError::InvalidEccentricity)
);
}
#[test]
fn mean_motion_rejects_hyperbolic() {
assert_eq!(
MeanMotionOrbit::try_new(
1.0 * AU,
1.1,
Degrees::new(0.0),
Degrees::new(0.0),
Degrees::new(0.0),
AngularRate::<Degree, Day>::new(1.0),
JulianDate::J2000,
),
Err(ConicError::HyperbolicNotSupported)
);
}
#[test]
fn conic_rejects_nan_epoch() {
assert_eq!(
ConicOrbit::try_new(
1.0 * AU,
0.5,
Degrees::new(0.0),
Degrees::new(0.0),
Degrees::new(0.0),
Degrees::new(0.0),
JulianDate::new(f64::NAN),
),
Err(ConicError::InvalidEpoch)
);
}
#[test]
fn conic_rejects_inf_mean_anomaly() {
assert_eq!(
ConicOrbit::try_new(
1.0 * AU,
0.5,
Degrees::new(0.0),
Degrees::new(0.0),
Degrees::new(0.0),
Degrees::new(f64::INFINITY),
JulianDate::J2000,
),
Err(ConicError::InvalidMeanAnomaly)
);
}
#[test]
fn conic_rejects_parabolic() {
assert_eq!(
ConicOrbit::try_new(
1.0 * AU,
1.0,
Degrees::new(0.0),
Degrees::new(0.0),
Degrees::new(0.0),
Degrees::new(0.0),
JulianDate::J2000,
),
Err(ConicError::ParabolicUnsupported)
);
}
#[test]
fn mean_motion_rejects_nan_mean_motion() {
assert_eq!(
MeanMotionOrbit::try_new(
1.0 * AU,
0.5,
Degrees::new(0.0),
Degrees::new(0.0),
Degrees::new(0.0),
AngularRate::<Degree, Day>::new(f64::NAN),
JulianDate::J2000,
),
Err(ConicError::InvalidMeanMotion)
);
}
#[test]
fn mean_motion_rejects_negative_mean_motion() {
assert_eq!(
MeanMotionOrbit::try_new(
1.0 * AU,
0.5,
Degrees::new(0.0),
Degrees::new(0.0),
Degrees::new(0.0),
AngularRate::<Degree, Day>::new(-1.0),
JulianDate::J2000,
),
Err(ConicError::InvalidMeanMotion)
);
}
#[test]
fn mean_motion_rejects_nan_epoch() {
assert_eq!(
MeanMotionOrbit::try_new(
1.0 * AU,
0.5,
Degrees::new(0.0),
Degrees::new(0.0),
Degrees::new(0.0),
AngularRate::<Degree, Day>::new(1.0),
JulianDate::new(f64::NAN),
),
Err(ConicError::InvalidEpoch)
);
}
}