use crate::PhysicsError;
use deep_causality_algebra::RealField;
use deep_causality_num::FromPrimitive;
#[derive(Clone, Copy, Debug)]
pub struct TwoBodyPropagator<R> {
gravitational_parameter: R,
semi_major_axis: R,
eccentricity: R,
mean_motion: R,
omega_peri: R,
ea0: R,
}
impl<R> TwoBodyPropagator<R>
where
R: RealField + FromPrimitive,
{
pub fn from_state(position: [R; 2], velocity: [R; 2], gm: R) -> Result<Self, PhysicsError> {
if gm <= R::zero() {
return Err(PhysicsError::PhysicalInvariantBroken(
"Gravitational parameter GM must be positive".into(),
));
}
let two = Self::lit(2.0)?;
let one = R::one();
let r = (position[0] * position[0] + position[1] * position[1]).sqrt();
if r <= R::zero() {
return Err(PhysicsError::Singularity("Radius must be positive".into()));
}
let v2 = velocity[0] * velocity[0] + velocity[1] * velocity[1];
let energy = v2 / two - gm / r;
if energy >= R::zero() {
return Err(PhysicsError::PhysicalInvariantBroken(
"State is not a bound orbit (energy >= 0); only ellipses are supported".into(),
));
}
let semi_major_axis = -gm / (two * energy);
let rv = position[0] * velocity[0] + position[1] * velocity[1];
let e_vec = [
((v2 - gm / r) * position[0] - rv * velocity[0]) / gm,
((v2 - gm / r) * position[1] - rv * velocity[1]) / gm,
];
let eccentricity = (e_vec[0] * e_vec[0] + e_vec[1] * e_vec[1]).sqrt();
if eccentricity >= one {
return Err(PhysicsError::PhysicalInvariantBroken(
"Non-elliptic orbit (e >= 1) is out of scope".into(),
));
}
let mean_motion = (gm / (semi_major_axis * semi_major_axis * semi_major_axis)).sqrt();
let omega_peri = e_vec[1].atan2(e_vec[0]);
let cos_nu = Self::clamp_unit(
(e_vec[0] * position[0] + e_vec[1] * position[1]) / (eccentricity * r),
);
let mut nu0 = cos_nu.acos();
if rv < R::zero() {
nu0 = two * R::pi() - nu0;
}
let ea0 = two
* ((one - eccentricity).sqrt() * (nu0 / two).sin())
.atan2((one + eccentricity).sqrt() * (nu0 / two).cos());
Ok(Self {
gravitational_parameter: gm,
semi_major_axis,
eccentricity,
mean_motion,
omega_peri,
ea0,
})
}
pub fn propagate(&self, dt: R) -> Result<([R; 2], [R; 2]), PhysicsError> {
let two = Self::lit(2.0)?;
let one = R::one();
let e = self.eccentricity;
let a = self.semi_major_axis;
let b = a * (one - e * e).sqrt();
let m0 = self.ea0 - e * self.ea0.sin();
let ea = Self::solve_kepler(m0 + self.mean_motion * dt, e, two)?;
let x_pf = a * (ea.cos() - e);
let y_pf = b * ea.sin();
let edot = self.mean_motion / (one - e * ea.cos());
let xdot_pf = -a * ea.sin() * edot;
let ydot_pf = b * ea.cos() * edot;
let (c, s) = (self.omega_peri.cos(), self.omega_peri.sin());
let position = [c * x_pf - s * y_pf, s * x_pf + c * y_pf];
let velocity = [c * xdot_pf - s * ydot_pf, s * xdot_pf + c * ydot_pf];
Ok((position, velocity))
}
pub fn semi_major_axis(&self) -> R {
self.semi_major_axis
}
pub fn eccentricity(&self) -> R {
self.eccentricity
}
pub fn mean_motion(&self) -> R {
self.mean_motion
}
pub fn gravitational_parameter(&self) -> R {
self.gravitational_parameter
}
pub fn period(&self) -> Result<R, PhysicsError> {
let two = Self::lit(2.0)?;
Ok(two * R::pi() / self.mean_motion)
}
fn lit(x: f64) -> Result<R, PhysicsError> {
R::from_f64(x)
.ok_or_else(|| PhysicsError::NumericalInstability("R::from_f64 failed".into()))
}
fn clamp_unit(x: R) -> R {
if x > R::one() {
R::one()
} else if x < -R::one() {
-R::one()
} else {
x
}
}
fn solve_kepler(m: R, e: R, two: R) -> Result<R, PhysicsError> {
let two_pi = two * R::pi();
let m = m - two_pi * (m / two_pi).floor();
let tol = Self::lit(1e-15)?;
let mut ea = m;
for _ in 0..100 {
let d = (ea - e * ea.sin() - m) / (R::one() - e * ea.cos());
ea -= d;
if d.abs() < tol {
break;
}
}
Ok(ea)
}
}