use core::marker::PhantomData;
use affn::centers::ReferenceCenter;
use affn::frames::ReferenceFrame;
use qtty::dynamics::GravitationalParameter;
use qtty::Second;
use crate::anomaly::{
hyperbolic_from_mean, hyperbolic_from_true, mean_from_hyperbolic, mean_from_true,
true_from_hyperbolic, true_from_mean, AnomalyError, AnomalyOptions, MeanAnomaly, TrueAnomaly,
};
use crate::elements::{ConicRegime, ConversionError, KeplerianElements};
use crate::state::CartesianState;
#[derive(Debug, Clone, Copy, PartialEq, thiserror::Error)]
pub enum PropagationError {
#[error("invalid eccentricity {0}")]
InvalidEccentricity(f64),
#[error("parabolic propagation is unsupported")]
ParabolicUnsupported,
#[error(transparent)]
AnomalyError(#[from] AnomalyError),
#[error(transparent)]
ConversionError(#[from] ConversionError),
}
#[derive(Debug, Clone, Copy)]
pub struct KeplerProblem<C: ReferenceCenter, F: ReferenceFrame> {
mu: GravitationalParameter,
_marker: PhantomData<(C, F)>,
}
impl<C: ReferenceCenter, F: ReferenceFrame> KeplerProblem<C, F> {
#[must_use]
pub fn new(mu: GravitationalParameter) -> Self {
Self {
mu,
_marker: PhantomData,
}
}
#[must_use]
pub fn mu(&self) -> GravitationalParameter {
self.mu
}
pub fn propagate(
&self,
state: &CartesianState<C, F>,
dt: Second,
) -> Result<CartesianState<C, F>, PropagationError>
where
C: ReferenceCenter<Params = ()>,
{
let el = KeplerianElements::<F>::from_cartesian(state, self.mu)?;
let a = el.semi_major_axis.value();
let ecc_value = el.eccentricity.value();
if !ecc_value.is_finite() || ecc_value < 0.0 {
return Err(PropagationError::InvalidEccentricity(ecc_value));
}
let true_anomaly = match el.conic_kind() {
ConicRegime::Elliptic => {
let n = (self.mu.value() / (a * a * a)).sqrt();
let m0 = mean_from_true(TrueAnomaly::new(el.true_anomaly), el.eccentricity);
let m = MeanAnomaly::from_value(m0.value() + n * dt.value());
true_from_mean(m, el.eccentricity, AnomalyOptions::default())?
}
ConicRegime::Hyperbolic => {
let n = (-self.mu.value() / (a * a * a)).sqrt();
let f0 = hyperbolic_from_true(TrueAnomaly::new(el.true_anomaly), el.eccentricity);
let m0 = mean_from_hyperbolic(f0, el.eccentricity);
let m = MeanAnomaly::from_value(m0.value() + n * dt.value());
true_from_hyperbolic(
hyperbolic_from_mean(m, el.eccentricity, AnomalyOptions::default())?,
el.eccentricity,
)
}
ConicRegime::Parabolic => return Err(PropagationError::ParabolicUnsupported),
};
let next = KeplerianElements::<F>::new(
el.semi_major_axis,
el.eccentricity,
el.inclination,
el.raan,
el.arg_periapsis,
true_anomaly.radians(),
)?;
Ok(next.try_to_cartesian::<C>(self.mu)?)
}
}
#[cfg(test)]
mod tests {
use super::*;
use affn::cartesian::{Position, Velocity};
use qtty::dynamics::KmPerSecond;
use qtty::length::Kilometer;
use crate::state::CartesianState;
use crate::transfer::{specific_angular_momentum, specific_orbital_energy};
#[derive(Debug, Copy, Clone)]
struct C;
impl ReferenceCenter for C {
type Params = ();
fn center_name() -> &'static str {
"C"
}
}
#[derive(Debug, Copy, Clone)]
struct F;
impl ReferenceFrame for F {
fn frame_name() -> &'static str {
"F"
}
}
#[test]
fn circular_returns_after_period() {
let mu = GravitationalParameter::new(398600.4418);
let r = 7000.0;
let state = CartesianState::<C, F>::new(
Position::<C, F, Kilometer>::new(r, 0.0, 0.0),
Velocity::<F, KmPerSecond>::new(0.0, (mu.value() / r).sqrt(), 0.0),
);
let period = 2.0 * core::f64::consts::PI * (r * r * r / mu.value()).sqrt();
let out = KeplerProblem::<C, F>::new(mu)
.propagate(&state, Second::new(period))
.unwrap();
assert!((out.position().x().value() - r).abs() < 1e-6);
assert!(out.position().y().value().abs() < 1e-5);
}
#[test]
fn invariants_are_conserved() {
let mu = GravitationalParameter::new(398600.4418);
let state = CartesianState::<C, F>::new(
Position::<C, F, Kilometer>::new(7000.0, 0.0, 0.0),
Velocity::<F, KmPerSecond>::new(0.0, 7.2, 1.0),
);
let out = KeplerProblem::<C, F>::new(mu)
.propagate(&state, Second::new(1000.0))
.unwrap();
assert!(
(specific_orbital_energy(&state, mu) - specific_orbital_energy(&out, mu))
.value()
.abs()
< 1e-8
);
assert!(
(specific_angular_momentum(&state) - specific_angular_momentum(&out))
.value()
.abs()
< 1e-8
);
}
#[test]
fn mu_accessor_returns_configured_mu() {
let mu = GravitationalParameter::new(398600.4418);
let p = KeplerProblem::<C, F>::new(mu);
assert_eq!(p.mu().value(), 398600.4418);
}
#[test]
fn hyperbolic_propagation_returns_finite_state() {
let mu = GravitationalParameter::new(398600.4418);
let r = 7000.0_f64;
let v_esc = (2.0 * mu.value() / r).sqrt();
let state = CartesianState::<C, F>::new(
Position::<C, F, Kilometer>::new(r, 0.0, 0.0),
Velocity::<F, KmPerSecond>::new(0.0, v_esc * 1.3, 0.0),
);
let result = KeplerProblem::<C, F>::new(mu).propagate(&state, Second::new(100.0));
assert!(result.is_ok(), "hyperbolic propagation failed: {result:?}");
}
#[test]
fn parabolic_state_returns_error() {
let mu = GravitationalParameter::new(398600.4418);
let r = 7000.0_f64;
let v_par = (2.0 * mu.value() / r).sqrt();
let state = CartesianState::<C, F>::new(
Position::<C, F, Kilometer>::new(r, 0.0, 0.0),
Velocity::<F, KmPerSecond>::new(0.0, v_par, 0.0),
);
assert!(KeplerProblem::<C, F>::new(mu)
.propagate(&state, Second::new(100.0))
.is_err());
}
}