use affn::centers::ReferenceCenter;
use affn::frames::ReferenceFrame;
use qtty::dynamics::{
GravitationalParameter, KmPerSeconds, SpecificAngularMomentum, SpecificOrbitalEnergy,
};
use qtty::length::Kilometers;
use qtty::Second;
use crate::problem::KeplerProblem;
use crate::state::CartesianState;
use crate::vec3::{cross, norm};
#[derive(Debug, Clone, Copy, PartialEq, thiserror::Error)]
pub enum TransferError {
#[error("invalid gravitational parameter: {0}")]
InvalidGravitationalParameter(f64),
#[error("invalid {0} radius: {1}")]
InvalidRadius(&'static str, f64),
#[error("invalid semi-major axis: {0}")]
InvalidSemiMajorAxis(f64),
#[error("non-finite result")]
NonFiniteResult,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct HohmannResult {
pub dv_departure: KmPerSeconds,
pub dv_arrival: KmPerSeconds,
pub total: KmPerSeconds,
pub transfer_time: Second,
}
#[must_use]
pub fn orbital_period<C: ReferenceCenter, F: ReferenceFrame>(
problem: &KeplerProblem<C, F>,
semi_major_axis: Kilometers,
) -> Option<Second> {
let a = semi_major_axis.value();
(a > 0.0).then(|| {
Second::new(2.0 * core::f64::consts::PI * (a * a * a / problem.mu().value()).sqrt())
})
}
#[must_use]
pub fn specific_orbital_energy<C: ReferenceCenter, F: ReferenceFrame>(
state: &CartesianState<C, F>,
mu: GravitationalParameter,
) -> SpecificOrbitalEnergy {
let r = [
state.position().x().value(),
state.position().y().value(),
state.position().z().value(),
];
let v = [
state.velocity().x().value(),
state.velocity().y().value(),
state.velocity().z().value(),
];
SpecificOrbitalEnergy::new(
0.5 * (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]) - mu.value() / norm(r),
)
}
#[must_use]
pub fn specific_angular_momentum<C: ReferenceCenter, F: ReferenceFrame>(
state: &CartesianState<C, F>,
) -> SpecificAngularMomentum {
let r = [
state.position().x().value(),
state.position().y().value(),
state.position().z().value(),
];
let v = [
state.velocity().x().value(),
state.velocity().y().value(),
state.velocity().z().value(),
];
SpecificAngularMomentum::new(norm(cross(r, v)))
}
#[must_use]
pub fn hohmann_delta_v(
mu: GravitationalParameter,
r1: Kilometers,
r2: Kilometers,
) -> HohmannResult {
let mu = mu.value();
let r1 = r1.value();
let r2 = r2.value();
let a_t = 0.5 * (r1 + r2);
let v1 = (mu / r1).sqrt();
let v2 = (mu / r2).sqrt();
let vt1 = (mu * (2.0 / r1 - 1.0 / a_t)).sqrt();
let vt2 = (mu * (2.0 / r2 - 1.0 / a_t)).sqrt();
let d1 = (vt1 - v1).abs();
let d2 = (v2 - vt2).abs();
HohmannResult {
dv_departure: KmPerSeconds::new(d1),
dv_arrival: KmPerSeconds::new(d2),
total: KmPerSeconds::new(d1 + d2),
transfer_time: Second::new(core::f64::consts::PI * (a_t * a_t * a_t / mu).sqrt()),
}
}
#[must_use]
pub fn vis_viva_speed(mu: GravitationalParameter, r: Kilometers, a: Kilometers) -> KmPerSeconds {
KmPerSeconds::new((mu.value() * (2.0 / r.value() - 1.0 / a.value())).sqrt())
}
#[must_use]
pub fn escape_speed(mu: GravitationalParameter, r: Kilometers) -> KmPerSeconds {
KmPerSeconds::new((2.0 * mu.value() / r.value()).sqrt())
}
pub fn try_orbital_period<C: ReferenceCenter, F: ReferenceFrame>(
problem: &KeplerProblem<C, F>,
semi_major_axis: Kilometers,
) -> Result<Second, TransferError> {
let mu = problem.mu().value();
if !mu.is_finite() || mu <= 0.0 {
return Err(TransferError::InvalidGravitationalParameter(mu));
}
let a = semi_major_axis.value();
if !a.is_finite() || a <= 0.0 {
return Err(TransferError::InvalidSemiMajorAxis(a));
}
let t = 2.0 * core::f64::consts::PI * (a * a * a / mu).sqrt();
if !t.is_finite() {
return Err(TransferError::NonFiniteResult);
}
Ok(Second::new(t))
}
pub fn try_hohmann_delta_v(
mu: GravitationalParameter,
r1: Kilometers,
r2: Kilometers,
) -> Result<HohmannResult, TransferError> {
let mu_val = mu.value();
if !mu_val.is_finite() || mu_val <= 0.0 {
return Err(TransferError::InvalidGravitationalParameter(mu_val));
}
let r1_val = r1.value();
if !r1_val.is_finite() || r1_val <= 0.0 {
return Err(TransferError::InvalidRadius("r1", r1_val));
}
let r2_val = r2.value();
if !r2_val.is_finite() || r2_val <= 0.0 {
return Err(TransferError::InvalidRadius("r2", r2_val));
}
let result = hohmann_delta_v(mu, r1, r2);
if !result.total.value().is_finite() {
return Err(TransferError::NonFiniteResult);
}
Ok(result)
}
pub fn try_vis_viva_speed(
mu: GravitationalParameter,
r: Kilometers,
a: Kilometers,
) -> Result<KmPerSeconds, TransferError> {
let mu_val = mu.value();
if !mu_val.is_finite() || mu_val <= 0.0 {
return Err(TransferError::InvalidGravitationalParameter(mu_val));
}
let r_val = r.value();
if !r_val.is_finite() || r_val <= 0.0 {
return Err(TransferError::InvalidRadius("r", r_val));
}
let a_val = a.value();
if !a_val.is_finite() || a_val == 0.0 {
return Err(TransferError::InvalidSemiMajorAxis(a_val));
}
let v = vis_viva_speed(mu, r, a);
if !v.value().is_finite() {
return Err(TransferError::NonFiniteResult);
}
Ok(v)
}
pub fn try_escape_speed(
mu: GravitationalParameter,
r: Kilometers,
) -> Result<KmPerSeconds, TransferError> {
let mu_val = mu.value();
if !mu_val.is_finite() || mu_val <= 0.0 {
return Err(TransferError::InvalidGravitationalParameter(mu_val));
}
let r_val = r.value();
if !r_val.is_finite() || r_val <= 0.0 {
return Err(TransferError::InvalidRadius("r", r_val));
}
let v = escape_speed(mu, r);
if !v.value().is_finite() {
return Err(TransferError::NonFiniteResult);
}
Ok(v)
}
#[cfg(test)]
mod tests {
use super::*;
use affn::cartesian::{Position, Velocity};
use qtty::dynamics::KmPerSecond;
use qtty::length::Kilometer;
#[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 helper_values_are_reasonable() {
let mu = GravitationalParameter::new(398600.4418);
let h = hohmann_delta_v(mu, Kilometers::new(6678.0), Kilometers::new(42164.0));
assert!((h.total.value() - 3.91).abs() < 0.2);
assert!((escape_speed(mu, Kilometers::new(6378.0)).value() - 11.18).abs() < 0.1);
assert!(
(vis_viva_speed(mu, Kilometers::new(7000.0), Kilometers::new(7000.0)).value() - 7.546)
.abs()
< 0.01
);
}
#[test]
fn invariants_compute() {
let s = CartesianState::<C, F>::new(
Position::<C, F, Kilometer>::new(7000.0, 0.0, 0.0),
Velocity::<F, KmPerSecond>::new(0.0, 7.5, 0.0),
);
assert!(
specific_orbital_energy(&s, GravitationalParameter::new(398600.4418)).value() < 0.0
);
assert!((specific_angular_momentum(&s).value() - 52500.0).abs() < 1e-12);
}
#[test]
fn specific_orbital_energy_and_period() {
let mu = GravitationalParameter::new(398600.4418);
let problem = KeplerProblem::<C, F>::new(mu);
let state = CartesianState::<C, F>::new(
Position::<C, F, Kilometer>::new(7000.0, 0.0, 0.0),
Velocity::<F, KmPerSecond>::new(0.0, 7.546, 0.0),
);
assert!(specific_orbital_energy(&state, mu).value() < 0.0);
assert!(specific_angular_momentum(&state).value() > 0.0);
let t = orbital_period(&problem, Kilometers::new(7000.0)).unwrap();
assert!((t.value() - 5840.0).abs() < 200.0);
}
#[test]
fn orbital_period_negative_sma_returns_none() {
let problem = KeplerProblem::<C, F>::new(GravitationalParameter::new(398600.4418));
assert!(orbital_period(&problem, Kilometers::new(-7000.0)).is_none());
}
#[test]
fn try_orbital_period_validates_inputs() {
let problem = KeplerProblem::<C, F>::new(GravitationalParameter::new(398600.4418));
let t = try_orbital_period(&problem, Kilometers::new(7000.0)).unwrap();
assert!((t.value() - 5840.0).abs() < 200.0);
assert!(matches!(
try_orbital_period(&problem, Kilometers::new(0.0)),
Err(TransferError::InvalidSemiMajorAxis(_))
));
assert!(matches!(
try_orbital_period(&problem, Kilometers::new(-7000.0)),
Err(TransferError::InvalidSemiMajorAxis(_))
));
}
#[test]
fn try_hohmann_delta_v_validates_inputs() {
let mu = GravitationalParameter::new(398600.4418);
let result =
try_hohmann_delta_v(mu, Kilometers::new(6678.0), Kilometers::new(42164.0)).unwrap();
assert!((result.total.value() - 3.91).abs() < 0.3);
assert!(matches!(
try_hohmann_delta_v(
GravitationalParameter::new(-1.0),
Kilometers::new(6678.0),
Kilometers::new(42164.0)
),
Err(TransferError::InvalidGravitationalParameter(_))
));
assert!(matches!(
try_hohmann_delta_v(
GravitationalParameter::new(0.0),
Kilometers::new(6678.0),
Kilometers::new(42164.0)
),
Err(TransferError::InvalidGravitationalParameter(_))
));
assert!(matches!(
try_hohmann_delta_v(mu, Kilometers::new(0.0), Kilometers::new(42164.0)),
Err(TransferError::InvalidRadius(_, _))
));
assert!(matches!(
try_hohmann_delta_v(mu, Kilometers::new(-6678.0), Kilometers::new(42164.0)),
Err(TransferError::InvalidRadius(_, _))
));
assert!(matches!(
try_hohmann_delta_v(mu, Kilometers::new(6678.0), Kilometers::new(-1.0)),
Err(TransferError::InvalidRadius(_, _))
));
}
#[test]
fn try_vis_viva_speed_validates_inputs() {
let mu = GravitationalParameter::new(398600.4418);
let v = try_vis_viva_speed(mu, Kilometers::new(7000.0), Kilometers::new(7000.0)).unwrap();
assert!((v.value() - 7.546).abs() < 0.05);
assert!(matches!(
try_vis_viva_speed(
GravitationalParameter::new(-1.0),
Kilometers::new(7000.0),
Kilometers::new(7000.0)
),
Err(TransferError::InvalidGravitationalParameter(_))
));
assert!(matches!(
try_vis_viva_speed(mu, Kilometers::new(0.0), Kilometers::new(7000.0)),
Err(TransferError::InvalidRadius(_, _))
));
assert!(matches!(
try_vis_viva_speed(mu, Kilometers::new(7000.0), Kilometers::new(0.0)),
Err(TransferError::InvalidSemiMajorAxis(_))
));
}
#[test]
fn try_escape_speed_validates_inputs() {
let mu = GravitationalParameter::new(398600.4418);
let v = try_escape_speed(mu, Kilometers::new(6378.0)).unwrap();
assert!((v.value() - 11.18).abs() < 0.1);
assert!(matches!(
try_escape_speed(GravitationalParameter::new(0.0), Kilometers::new(6378.0)),
Err(TransferError::InvalidGravitationalParameter(_))
));
assert!(matches!(
try_escape_speed(mu, Kilometers::new(-1.0)),
Err(TransferError::InvalidRadius(_, _))
));
assert!(matches!(
try_escape_speed(
GravitationalParameter::new(f64::NAN),
Kilometers::new(6378.0)
),
Err(TransferError::InvalidGravitationalParameter(_))
));
}
}