use crate::astro::dynamics::context::DynamicsContext;
use crate::astro::dynamics::forces::AccelerationModel;
use crate::astro::dynamics::frames::{LocalOrbitalFrame, RTN};
use crate::astro::dynamics::state::{Acceleration, AccelerationUnit, OrbitState};
use crate::coordinates::centers::Geocentric;
use crate::coordinates::frames::GCRS;
use crate::time::JulianDate;
use principia::PrincipiaError;
use qtty::{KmPerSecondsSquared, Second};
use tempoch::{JD, TT};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PeriodicHarmonic {
OncePerRev,
TwicePerRev,
}
impl PeriodicHarmonic {
pub fn multiplier(self) -> f64 {
match self {
PeriodicHarmonic::OncePerRev => 1.0,
PeriodicHarmonic::TwicePerRev => 2.0,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct EmpiricalPeriodicAcceleration {
pub harmonic: PeriodicHarmonic,
pub epoch_ref: JulianDate,
pub period: Second,
pub r_cos: KmPerSecondsSquared,
pub r_sin: KmPerSecondsSquared,
pub t_cos: KmPerSecondsSquared,
pub t_sin: KmPerSecondsSquared,
pub n_cos: KmPerSecondsSquared,
pub n_sin: KmPerSecondsSquared,
}
impl EmpiricalPeriodicAcceleration {
#[allow(clippy::too_many_arguments)]
pub fn new(
harmonic: PeriodicHarmonic,
epoch_ref: JulianDate,
period: Second,
r_cos: KmPerSecondsSquared,
r_sin: KmPerSecondsSquared,
t_cos: KmPerSecondsSquared,
t_sin: KmPerSecondsSquared,
n_cos: KmPerSecondsSquared,
n_sin: KmPerSecondsSquared,
) -> Self {
Self {
harmonic,
epoch_ref,
period,
r_cos,
r_sin,
t_cos,
t_sin,
n_cos,
n_sin,
}
}
fn phase(&self, epoch: JulianDate) -> f64 {
let dt_days = epoch.value() - self.epoch_ref.value();
let dt_s = dt_days * 86_400.0;
let t = self.period.value();
if t <= 0.0 || !t.is_finite() {
return 0.0;
}
let omega = 2.0 * core::f64::consts::PI / t;
self.harmonic.multiplier() * omega * dt_s
}
}
impl AccelerationModel<DynamicsContext, TT, Geocentric, GCRS> for EmpiricalPeriodicAcceleration {
fn name(&self) -> &'static str {
"empirical_periodic"
}
fn acceleration(
&self,
s: &OrbitState,
_ctx: &DynamicsContext,
) -> Result<Acceleration<GCRS, AccelerationUnit>, PrincipiaError> {
let theta = self.phase(s.epoch.to::<JD>());
let (sin_t, cos_t) = theta.sin_cos();
let a_rtn = [
self.r_cos.value() * cos_t + self.r_sin.value() * sin_t,
self.t_cos.value() * cos_t + self.t_sin.value() * sin_t,
self.n_cos.value() * cos_t + self.n_sin.value() * sin_t,
];
let rtn_frame = LocalOrbitalFrame::<RTN>::try_from_state(s)?;
let a_gcrs = rtn_frame.rotation_inverse().apply_array(a_rtn);
Ok(Acceleration::<GCRS, AccelerationUnit>::new(
a_gcrs[0], a_gcrs[1], a_gcrs[2],
))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::astro::dynamics::{Position, Velocity};
use crate::coordinates::frames::GCRS;
fn s0() -> OrbitState {
OrbitState::new(
JulianDate::new(2_451_545.0).to_j2000s(),
Position::<GCRS>::new(7_000.0, 0.0, 0.0),
Velocity::<GCRS>::new(0.0, 7.5450, 0.0),
)
}
#[test]
fn one_cpr_at_reference_epoch_returns_cosine_amplitude() {
let f = EmpiricalPeriodicAcceleration::new(
PeriodicHarmonic::OncePerRev,
JulianDate::new(2_451_545.0),
Second::new(5400.0),
KmPerSecondsSquared::new(1e-9),
KmPerSecondsSquared::new(0.0),
KmPerSecondsSquared::new(0.0),
KmPerSecondsSquared::new(0.0),
KmPerSecondsSquared::new(0.0),
KmPerSecondsSquared::new(0.0),
);
let a = f.acceleration(&s0(), &DynamicsContext::empty()).unwrap();
assert!((a.x().value() - 1e-9).abs() < 1e-15);
}
#[test]
fn two_cpr_doubles_phase() {
let f1 = EmpiricalPeriodicAcceleration::new(
PeriodicHarmonic::OncePerRev,
JulianDate::new(2_451_545.0),
Second::new(5400.0),
KmPerSecondsSquared::new(0.0),
KmPerSecondsSquared::new(0.0),
KmPerSecondsSquared::new(1e-9),
KmPerSecondsSquared::new(0.0),
KmPerSecondsSquared::new(0.0),
KmPerSecondsSquared::new(0.0),
);
let mut f2 = f1;
f2.harmonic = PeriodicHarmonic::TwicePerRev;
let dt_quarter = 5400.0 / 4.0;
let epoch = JulianDate::new(2_451_545.0 + dt_quarter / 86_400.0);
let mut s = s0();
s.epoch = epoch.into();
let a1 = f1.acceleration(&s, &DynamicsContext::empty()).unwrap();
let a2 = f2.acceleration(&s, &DynamicsContext::empty()).unwrap();
let mag1 = (a1.x().value().powi(2) + a1.y().value().powi(2)).sqrt();
let mag2 = (a2.x().value().powi(2) + a2.y().value().powi(2)).sqrt();
assert!(mag1 < 1e-13, "1cpr mag at T/4 should be ≈0, got {mag1:e}");
assert!(
(mag2 - 1e-9).abs() < 1e-12,
"2cpr mag at T/4 should be ≈1e-9, got {mag2:e}"
);
}
}