use astrodyn_quantities::aliases::Position;
use astrodyn_quantities::dims::GravParam;
use astrodyn_quantities::frame::{RootInertial, SelfPlanet};
use glam::{DMat3, DVec3};
use uom::si::f64::{Length, Ratio};
pub const EARTH_K2: f64 = 0.29525;
#[derive(Debug, Clone, PartialEq)]
pub struct TidalConfig {
pub k2: f64,
pub mu_primary: f64,
pub radius_primary: f64,
pub tidal_bodies: Vec<TidalBody>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct TidalBody {
pub mu: f64,
pub position_inertial: DVec3,
}
#[derive(Debug, Clone)]
pub struct TidalConfigTyped {
pub k2: Ratio,
pub mu_primary: GravParam<SelfPlanet>,
pub radius_primary: Length,
pub tidal_bodies: Vec<TidalBodyTyped>,
}
#[derive(Debug, Clone)]
pub struct TidalBodyTyped {
pub mu: GravParam<SelfPlanet>,
pub position_inertial: Position<RootInertial>,
}
impl TidalBodyTyped {
#[inline]
pub fn to_untyped(&self) -> TidalBody {
TidalBody {
mu: self.mu.value,
position_inertial: self.position_inertial.raw_si(),
}
}
#[inline]
pub fn from_untyped_unchecked(b: &TidalBody) -> Self {
Self {
mu: GravParam::<SelfPlanet>::from_si(b.mu),
position_inertial: Position::<RootInertial>::from_raw_si(b.position_inertial),
}
}
}
impl TidalConfigTyped {
pub fn to_untyped(&self) -> TidalConfig {
TidalConfig {
k2: self.k2.value,
mu_primary: self.mu_primary.value,
radius_primary: self.radius_primary.value,
tidal_bodies: self
.tidal_bodies
.iter()
.map(|b| TidalBody {
mu: b.mu.value,
position_inertial: b.position_inertial.raw_si(),
})
.collect(),
}
}
pub fn from_untyped(config: &TidalConfig) -> Self {
use astrodyn_quantities::ext::{F64Ext, Vec3Ext};
Self {
k2: Ratio::new::<uom::si::ratio::ratio>(config.k2),
mu_primary: F64Ext::m3_per_s2(config.mu_primary),
radius_primary: Length::new::<uom::si::length::meter>(config.radius_primary),
tidal_bodies: config
.tidal_bodies
.iter()
.map(|b| TidalBodyTyped {
mu: F64Ext::m3_per_s2(b.mu),
position_inertial: b.position_inertial.m_at::<RootInertial>(),
})
.collect(),
}
}
}
pub fn compute_delta_c20_typed(config: &TidalConfigTyped, t_inertial_pfix: &DMat3) -> Ratio {
let raw = compute_delta_c20(&config.to_untyped(), t_inertial_pfix);
Ratio::new::<uom::si::ratio::ratio>(raw)
}
pub fn compute_delta_c20(config: &TidalConfig, t_inertial_pfix: &DMat3) -> f64 {
let sqrt5 = 5.0_f64.sqrt();
let mut f = 0.0;
for body in &config.tidal_bodies {
let pfix_position = *t_inertial_pfix * body.position_inertial;
let r = pfix_position.length();
if r == 0.0 {
continue; }
let sin_phi = pfix_position.z / r; let r_over_r = config.radius_primary / r;
let r_over_r_3 = r_over_r * r_over_r * r_over_r;
f += body.mu / config.mu_primary * r_over_r_3 * sqrt5 * (1.5 * sin_phi * sin_phi - 0.5);
}
config.k2 / 5.0 * f
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn delta_c20_zero_for_no_tidal_bodies() {
let config = TidalConfig {
k2: EARTH_K2,
mu_primary: 3.986e14,
radius_primary: 6.378e6,
tidal_bodies: vec![],
};
let delta = compute_delta_c20(&config, &DMat3::IDENTITY);
assert_eq!(delta, 0.0);
}
#[test]
fn delta_c20_nonzero_for_moon() {
let config = TidalConfig {
k2: EARTH_K2,
mu_primary: 3.986004415e14,
radius_primary: 6_378_137.0,
tidal_bodies: vec![TidalBody {
mu: 4902.79980693169e9,
position_inertial: DVec3::new(0.0, 0.0, 384_400_000.0),
}],
};
let delta = compute_delta_c20(&config, &DMat3::IDENTITY);
assert!(delta.abs() > 1e-12, "ΔC20 too small: {delta}");
assert!(delta.abs() < 1e-6, "ΔC20 too large: {delta}");
println!("ΔC20 (Moon at pole): {delta:.6e}");
}
#[test]
fn delta_c20_at_equator_is_negative() {
let config = TidalConfig {
k2: EARTH_K2,
mu_primary: 3.986004415e14,
radius_primary: 6_378_137.0,
tidal_bodies: vec![TidalBody {
mu: 4902.79980693169e9,
position_inertial: DVec3::new(384_400_000.0, 0.0, 0.0),
}],
};
let delta = compute_delta_c20(&config, &DMat3::IDENTITY);
assert!(delta < 0.0, "ΔC20 should be negative at equator: {delta}");
}
#[test]
fn delta_c20_at_pole_is_positive() {
let config = TidalConfig {
k2: EARTH_K2,
mu_primary: 3.986004415e14,
radius_primary: 6_378_137.0,
tidal_bodies: vec![TidalBody {
mu: 4902.79980693169e9,
position_inertial: DVec3::new(0.0, 0.0, 384_400_000.0),
}],
};
let delta = compute_delta_c20(&config, &DMat3::IDENTITY);
assert!(delta > 0.0, "ΔC20 should be positive at pole: {delta}");
}
#[test]
fn compute_delta_c20_typed_matches_untyped() {
use astrodyn_quantities::ext::F64Ext;
use uom::si::length::meter;
use uom::si::ratio::ratio;
let untyped = TidalConfig {
k2: EARTH_K2,
mu_primary: 3.986004415e14,
radius_primary: 6_378_137.0,
tidal_bodies: vec![TidalBody {
mu: 4902.79980693169e9,
position_inertial: DVec3::new(0.0, 0.0, 384_400_000.0),
}],
};
let typed = TidalConfigTyped {
k2: Ratio::new::<ratio>(untyped.k2),
mu_primary: untyped.mu_primary.m3_per_s2(),
radius_primary: Length::new::<meter>(untyped.radius_primary),
tidal_bodies: untyped
.tidal_bodies
.iter()
.map(|b| TidalBodyTyped {
mu: b.mu.m3_per_s2(),
position_inertial: Position::<RootInertial>::from_raw_si(b.position_inertial),
})
.collect(),
};
let untyped_delta = compute_delta_c20(&untyped, &DMat3::IDENTITY);
let typed_delta = compute_delta_c20_typed(&typed, &DMat3::IDENTITY);
assert_eq!(typed_delta.value, untyped_delta);
}
use proptest::prelude::*;
fn arb_finite_bounded() -> impl Strategy<Value = f64> {
prop_oneof![
(1.0e-9_f64..1.0e9_f64),
(1.0e-9_f64..1.0e9_f64).prop_map(|x| -x),
]
}
fn arb_dvec3() -> impl Strategy<Value = DVec3> {
(
arb_finite_bounded(),
arb_finite_bounded(),
arb_finite_bounded(),
)
.prop_map(|(x, y, z)| DVec3::new(x, y, z))
}
fn arb_tidal_body() -> impl Strategy<Value = TidalBody> {
(arb_finite_bounded(), arb_dvec3()).prop_map(|(mu, position_inertial)| TidalBody {
mu,
position_inertial,
})
}
fn arb_tidal_config() -> impl Strategy<Value = TidalConfig> {
(
arb_finite_bounded(),
arb_finite_bounded(),
arb_finite_bounded(),
proptest::collection::vec(arb_tidal_body(), 0..=4),
)
.prop_map(
|(k2, mu_primary, radius_primary, tidal_bodies)| TidalConfig {
k2,
mu_primary,
radius_primary,
tidal_bodies,
},
)
}
proptest! {
#[test]
fn round_trip_tidal_body_untyped_typed_untyped(orig in arb_tidal_body()) {
let typed = TidalBodyTyped::from_untyped_unchecked(&orig);
prop_assert_eq!(typed.to_untyped(), orig);
}
#[test]
fn round_trip_tidal_body_typed_untyped_typed(orig in arb_tidal_body()) {
let typed = TidalBodyTyped::from_untyped_unchecked(&orig);
let lifted = TidalBodyTyped::from_untyped_unchecked(&typed.to_untyped());
prop_assert_eq!(lifted.to_untyped(), typed.to_untyped());
}
#[test]
fn round_trip_tidal_config_untyped_typed_untyped(orig in arb_tidal_config()) {
let typed = TidalConfigTyped::from_untyped(&orig);
prop_assert_eq!(typed.to_untyped(), orig);
}
#[test]
fn round_trip_tidal_config_typed_untyped_typed(orig in arb_tidal_config()) {
let typed = TidalConfigTyped::from_untyped(&orig);
let lifted = TidalConfigTyped::from_untyped(&typed.to_untyped());
prop_assert_eq!(lifted.to_untyped(), typed.to_untyped());
}
}
}