use core::f64::consts::PI;
use core::marker::PhantomData;
use affn::cartesian::{Position, Velocity};
use affn::centers::ReferenceCenter;
use affn::frames::ReferenceFrame;
use qtty::angular::Radians;
use qtty::dynamics::{GravitationalParameter, KmPerSecond};
use qtty::length::{Kilometer, Kilometers};
use crate::anomaly::wrap_two_pi_raw;
use crate::eccentricity::Eccentricity;
use crate::state::CartesianState;
use crate::vec3::{cross, dot, norm, scale, sub};
const EPS: f64 = 1.0e-10;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConicRegime {
Elliptic,
Parabolic,
Hyperbolic,
}
#[derive(Debug, Clone, Copy, PartialEq, thiserror::Error)]
pub enum ConversionError {
#[error("invalid eccentricity {0}")]
InvalidEccentricity(f64),
#[error("invalid inclination {0}")]
InvalidInclination(f64),
#[error("non-finite {field}: {value}")]
NonFiniteValue {
field: &'static str,
value: f64,
},
#[error("degenerate orbital geometry: {0}")]
Degenerate(&'static str),
}
#[derive(Debug, Clone, Copy)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct KeplerianElements<F: ReferenceFrame> {
pub semi_major_axis: Kilometers,
pub eccentricity: Eccentricity,
pub inclination: Radians,
pub raan: Radians,
pub arg_periapsis: Radians,
pub true_anomaly: Radians,
_frame: PhantomData<F>,
}
impl<F: ReferenceFrame> KeplerianElements<F> {
pub fn new(
semi_major_axis: Kilometers,
eccentricity: Eccentricity,
inclination: Radians,
raan: Radians,
arg_periapsis: Radians,
true_anomaly: Radians,
) -> Result<Self, ConversionError> {
validate_finite("semi_major_axis", semi_major_axis.value())?;
validate_finite("eccentricity", eccentricity.value())?;
validate_finite("inclination", inclination.value())?;
validate_finite("raan", raan.value())?;
validate_finite("arg_periapsis", arg_periapsis.value())?;
validate_finite("true_anomaly", true_anomaly.value())?;
if eccentricity.value() < 0.0 {
return Err(ConversionError::InvalidEccentricity(eccentricity.value()));
}
if !(0.0..=PI).contains(&inclination.value()) {
return Err(ConversionError::InvalidInclination(inclination.value()));
}
Ok(Self {
semi_major_axis,
eccentricity,
inclination,
raan: Radians::new(wrap_two_pi_raw(raan.value())),
arg_periapsis: Radians::new(wrap_two_pi_raw(arg_periapsis.value())),
true_anomaly: Radians::new(wrap_two_pi_raw(true_anomaly.value())),
_frame: PhantomData,
})
}
#[must_use]
pub fn conic_kind(&self) -> ConicRegime {
if self.eccentricity.is_parabolic(EPS) {
ConicRegime::Parabolic
} else if self.eccentricity.is_elliptic() {
ConicRegime::Elliptic
} else {
ConicRegime::Hyperbolic
}
}
#[must_use]
pub fn to_cartesian<C: ReferenceCenter<Params = ()>>(
&self,
mu: GravitationalParameter,
) -> CartesianState<C, F> {
let a = self.semi_major_axis.value();
let e = self.eccentricity.value();
let nu = self.true_anomaly.value();
let p = a * (1.0 - e * e);
let denom = 1.0 + e * nu.cos();
let r = p / denom;
let root = (mu.value() / p).sqrt();
let r_pqw = [r * nu.cos(), r * nu.sin(), 0.0];
let v_pqw = [-root * nu.sin(), root * (e + nu.cos()), 0.0];
let r_ijk = rotate_pqw(
r_pqw,
self.raan.value(),
self.inclination.value(),
self.arg_periapsis.value(),
);
let v_ijk = rotate_pqw(
v_pqw,
self.raan.value(),
self.inclination.value(),
self.arg_periapsis.value(),
);
CartesianState::new(
Position::<C, F, Kilometer>::new(r_ijk[0], r_ijk[1], r_ijk[2]),
Velocity::<F, KmPerSecond>::new(v_ijk[0], v_ijk[1], v_ijk[2]),
)
}
pub fn from_cartesian<C: ReferenceCenter>(
state: &CartesianState<C, F>,
mu: GravitationalParameter,
) -> Result<Self, ConversionError> {
let r = vec3_from_pos(state.position());
let v = vec3_from_vel(state.velocity());
let mu = mu.value();
validate_finite("mu", mu)?;
if mu <= 0.0 {
return Err(ConversionError::Degenerate(
"non-positive gravitational parameter",
));
}
let rmag = norm(r);
let vmag = norm(v);
if rmag <= EPS {
return Err(ConversionError::Degenerate("zero position"));
}
let h = cross(r, v);
let hmag = norm(h);
if hmag <= EPS {
return Err(ConversionError::Degenerate("zero angular momentum"));
}
let n = [-h[1], h[0], 0.0];
let nmag = norm(n);
let e_vec = sub(scale(cross(v, h), 1.0 / mu), scale(r, 1.0 / rmag));
let ecc_value = norm(e_vec);
let energy = 0.5 * vmag * vmag - mu / rmag;
if energy.abs() <= EPS {
return Err(ConversionError::Degenerate("parabolic orbit"));
}
let a = -mu / (2.0 * energy);
let inc = (h[2] / hmag).clamp(-1.0, 1.0).acos();
let raan = if nmag > EPS {
wrap_two_pi_raw(n[1].atan2(n[0]))
} else {
0.0
};
let hhat = scale(h, 1.0 / hmag);
let argp = if nmag > EPS && ecc_value > EPS {
wrap_two_pi_raw(dot(cross(n, e_vec), hhat).atan2(dot(n, e_vec)))
} else if ecc_value > EPS {
wrap_two_pi_raw(e_vec[1].atan2(e_vec[0]))
} else {
0.0
};
let nu = if ecc_value > EPS {
wrap_two_pi_raw(
(dot(cross(e_vec, r), hhat) / (ecc_value * rmag))
.atan2(dot(e_vec, r) / (ecc_value * rmag)),
)
} else if nmag > EPS {
wrap_two_pi_raw(
(dot(cross(n, r), hhat) / (nmag * rmag)).atan2(dot(n, r) / (nmag * rmag)),
)
} else {
wrap_two_pi_raw(r[1].atan2(r[0]))
};
let eccentricity =
Eccentricity::new(ecc_value).ok_or(ConversionError::InvalidEccentricity(ecc_value))?;
Self::new(
Kilometers::new(a),
eccentricity,
Radians::new(inc),
Radians::new(raan),
Radians::new(argp),
Radians::new(nu),
)
}
}
fn validate_finite(field: &'static str, value: f64) -> Result<(), ConversionError> {
if value.is_finite() {
Ok(())
} else {
Err(ConversionError::NonFiniteValue { field, value })
}
}
fn vec3_from_pos<C: ReferenceCenter, F: ReferenceFrame>(p: &Position<C, F, Kilometer>) -> [f64; 3] {
[p.x().value(), p.y().value(), p.z().value()]
}
fn vec3_from_vel<F: ReferenceFrame>(v: &Velocity<F, KmPerSecond>) -> [f64; 3] {
[v.x().value(), v.y().value(), v.z().value()]
}
fn rotate_pqw(v: [f64; 3], raan: f64, inc: f64, argp: f64) -> [f64; 3] {
let (co, so) = (raan.cos(), raan.sin());
let (ci, si) = (inc.cos(), inc.sin());
let (cw, sw) = (argp.cos(), argp.sin());
let r11 = co * cw - so * sw * ci;
let r12 = -co * sw - so * cw * ci;
let r21 = so * cw + co * sw * ci;
let r22 = -so * sw + co * cw * ci;
let r31 = sw * si;
let r32 = cw * si;
[
r11 * v[0] + r12 * v[1],
r21 * v[0] + r22 * v[1],
r31 * v[0] + r32 * v[1],
]
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug, Copy, Clone)]
struct Center;
impl ReferenceCenter for Center {
type Params = ();
fn center_name() -> &'static str {
"C"
}
}
#[derive(Debug, Copy, Clone)]
struct Frame;
impl ReferenceFrame for Frame {
fn frame_name() -> &'static str {
"F"
}
}
#[test]
fn circular_equatorial_case() {
let state = CartesianState::<Center, Frame>::new(
Position::new(7000.0, 0.0, 0.0),
Velocity::new(0.0, (398600.4418_f64 / 7000.0).sqrt(), 0.0),
);
let el =
KeplerianElements::from_cartesian(&state, GravitationalParameter::new(398600.4418))
.unwrap();
assert!(el.eccentricity.value() < 1e-12);
assert!(el.inclination.value().abs() < 1e-12);
assert!((el.semi_major_axis.value() - 7000.0).abs() < 1e-8);
}
#[test]
fn round_trip_moderate_eccentricity() {
let el = KeplerianElements::<Frame>::new(
Kilometers::new(12000.0),
Eccentricity::new(0.2).unwrap(),
Radians::new(0.4),
Radians::new(0.3),
Radians::new(0.7),
Radians::new(1.1),
)
.unwrap();
let st = el.to_cartesian::<Center>(GravitationalParameter::new(398600.4418));
let back = KeplerianElements::from_cartesian(&st, GravitationalParameter::new(398600.4418))
.unwrap();
assert!((back.semi_major_axis.value() - el.semi_major_axis.value()).abs() < 1e-8);
assert!((back.eccentricity.value() - el.eccentricity.value()).abs() < 1e-12);
}
}