use crate::cio::earth_rotation_angle;
use crate::egm2008_data::{EGM2008_GM, EGM2008_RE};
use crate::ephem::{moon_position, sun_position};
use crate::fes2004_data::FES2004;
use crate::forces::{MU_MOON, MU_SUN};
use crate::gravity_sh::SphericalHarmonicField;
use crate::nutation::delaunay_args;
use std::collections::BTreeMap;
type Vec3 = [f64; 3];
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct StokesDelta {
pub n: usize,
pub m: usize,
pub dc: f64,
pub ds: f64,
}
const K20: (f64, f64) = (0.30190, 0.0);
const K21: (f64, f64) = (0.29830, -0.00144);
#[allow(clippy::approx_constant)]
const K22: (f64, f64) = (0.30102, -0.00130);
const K30: f64 = 0.093;
const K31: f64 = 0.093;
const K32: f64 = 0.093;
const K33: f64 = 0.094;
const A0: f64 = 4.4228e-8; const H0: f64 = -0.31460;
pub fn permanent_tide_c20() -> f64 {
A0 * H0 * K20.0
}
pub fn pbar(n: usize, m: usize, u: f64) -> f64 {
let w = (1.0 - u * u).max(0.0).sqrt(); match (n, m) {
(2, 0) => 5.0_f64.sqrt() * (3.0 * u * u - 1.0) / 2.0,
(2, 1) => 15.0_f64.sqrt() * u * w,
(2, 2) => 15.0_f64.sqrt() / 2.0 * (1.0 - u * u),
(3, 0) => 7.0_f64.sqrt() * (5.0 * u * u * u - 3.0 * u) / 2.0,
(3, 1) => (7.0_f64 / 6.0).sqrt() * 1.5 * (5.0 * u * u - 1.0) * w,
(3, 2) => (7.0_f64 / 60.0).sqrt() * 15.0 * u * (1.0 - u * u),
(3, 3) => (7.0_f64 / 360.0).sqrt() * 15.0 * w * w * w,
_ => 0.0,
}
}
fn love(n: usize, m: usize) -> (f64, f64) {
match (n, m) {
(2, 0) => K20,
(2, 1) => K21,
(2, 2) => K22,
(3, 0) => (K30, 0.0),
(3, 1) => (K31, 0.0),
(3, 2) => (K32, 0.0),
(3, 3) => (K33, 0.0),
_ => (0.0, 0.0),
}
}
pub fn step2_constituent(
m: usize,
a_m: f64,
dk_re: f64,
dk_im: f64,
h_f: f64,
theta_f: f64,
) -> (f64, f64) {
let amp = a_m * h_f;
let (tr, ti) = (amp * dk_re, amp * dk_im); let (s, c) = theta_f.sin_cos();
let pr = tr * c - ti * s;
let pi = tr * s + ti * c;
match m {
1 => (pi, pr),
2 => (pr, -pi),
_ => (0.0, 0.0),
}
}
pub fn solid_earth_tide_step1(jd_tt: f64) -> Vec<StokesDelta> {
let t_jc = (jd_tt - 2_451_545.0) / 36525.0;
let theta_g = earth_rotation_angle(jd_tt);
let bodies = [(sun_position(t_jc), MU_SUN), (moon_position(t_jc), MU_MOON)];
let geo: Vec<(f64, f64, f64, f64)> = bodies
.iter()
.map(|&(p, mu)| {
let r = (p[0] * p[0] + p[1] * p[1] + p[2] * p[2]).sqrt();
let sinphi = p[2] / r;
let lambda = p[1].atan2(p[0]) - theta_g;
(sinphi, lambda, r, mu)
})
.collect();
let mut out = Vec::new();
for (n, mmax) in [(2usize, 2usize), (3, 3)] {
for m in 0..=mmax {
let (kr, ki) = love(n, m);
let mut sum_cos = 0.0;
let mut sum_sin = 0.0;
for &(sinphi, lambda, r, mu) in &geo {
let g =
(mu / EGM2008_GM) * (EGM2008_RE / r).powi((n + 1) as i32) * pbar(n, m, sinphi);
let ml = m as f64 * lambda;
sum_cos += g * ml.cos();
sum_sin += g * ml.sin();
}
let f = 1.0 / (2.0 * n as f64 + 1.0);
let dc = f * (kr * sum_cos + ki * sum_sin);
let ds = f * (kr * sum_sin - ki * sum_cos);
out.push(StokesDelta { n, m, dc, ds });
}
}
out
}
pub fn doodson_args(jd_tt: f64) -> [f64; 6] {
let [l, lp, f, d, om] = delaunay_args(jd_tt);
let theta_g = earth_rotation_angle(jd_tt);
let s = f + om;
let h = s - d;
let p = s - l;
let np = -om;
let ps = h - lp;
let tau = (theta_g + std::f64::consts::PI) - s;
[tau, s, h, p, np, ps]
}
pub fn doodson_phase(mult: &[i8; 6], args: &[f64; 6]) -> f64 {
mult.iter().zip(args).map(|(&k, &a)| k as f64 * a).sum()
}
pub fn ocean_tide(jd_tt: f64) -> Vec<StokesDelta> {
let args = doodson_args(jd_tt);
let mut acc: BTreeMap<(usize, usize), (f64, f64)> = BTreeMap::new();
for &(mult, n, m, cp, sp, cm, sm) in FES2004 {
let theta = doodson_phase(&mult, &args);
let (sin_t, cos_t) = theta.sin_cos();
let dc = ((cp + cm) * cos_t + (sp + sm) * sin_t) * 1e-11;
let ds = ((cm - cp) * sin_t + (sp - sm) * cos_t) * 1e-11;
let e = acc.entry((n as usize, m as usize)).or_insert((0.0, 0.0));
e.0 += dc;
e.1 += ds;
}
acc.into_iter()
.map(|((n, m), (dc, ds))| StokesDelta { n, m, dc, ds })
.collect()
}
type S2AirRow = (u8, u8, f64, f64, f64, f64);
static S2_AIR: &[S2AirRow] = &[
(2, 0, 51.79, 324.08, 0.0, 0.0),
(2, 1, 4.08, 200.38, 20.79, 49.41),
(2, 2, 365.07, 292.85, 6.21, 292.80),
(3, 0, 36.41, 341.75, 0.0, 0.0),
(3, 1, 2.32, 230.91, 6.35, 245.30),
(3, 2, 7.80, 22.93, 3.54, 296.31),
(3, 3, 3.75, 18.18, 1.01, 288.25),
(4, 0, 16.60, 91.68, 0.0, 0.0),
(4, 1, 2.80, 327.47, 3.10, 239.41),
(4, 2, 16.43, 118.97, 2.33, 127.63),
(4, 3, 0.40, 26.49, 0.51, 338.93),
(4, 4, 0.15, 91.20, 0.07, 227.75),
];
const S2_DOODSON: [i8; 6] = [2, 2, -2, 0, 0, 0];
fn load_love(n: usize) -> f64 {
match n {
2 => -0.3075,
3 => -0.195,
4 => -0.132,
_ => 0.0,
}
}
pub fn atmospheric_tide(jd_tt: f64) -> Vec<StokesDelta> {
const G: f64 = 6.674e-11; const GE: f64 = 9.806_65; const UBAR_TO_PA: f64 = 0.1; let args = doodson_args(jd_tt);
let (sin_t, cos_t) = doodson_phase(&S2_DOODSON, &args).sin_cos();
let mut out = Vec::new();
for &(n8, m8, dp, psp, dm, psm) in S2_AIR {
let (n, m) = (n8 as usize, m8 as usize);
let (cp, sp) = (dp * psp.to_radians().sin(), dp * psp.to_radians().cos());
let (cm, sm) = (dm * psm.to_radians().sin(), dm * psm.to_radians().cos());
let fac = (4.0 * std::f64::consts::PI * G / (GE * GE))
* ((1.0 + load_love(n)) / (2.0 * n as f64 + 1.0))
* UBAR_TO_PA;
let dc = fac * ((cp + cm) * cos_t + (sp + sm) * sin_t);
let ds = fac * ((cm - cp) * sin_t + (sp - sm) * cos_t);
out.push(StokesDelta { n, m, dc, ds });
}
out
}
fn rot_eci_to_ecef(theta_g: f64, v: Vec3) -> Vec3 {
let (s, c) = theta_g.sin_cos();
[c * v[0] + s * v[1], -s * v[0] + c * v[1], v[2]]
}
fn rot_ecef_to_eci(theta_g: f64, v: Vec3) -> Vec3 {
let (s, c) = theta_g.sin_cos();
[c * v[0] - s * v[1], s * v[0] + c * v[1], v[2]]
}
pub fn tidal_acceleration(r_eci: Vec3, jd_tt: f64) -> Vec3 {
let theta_g = earth_rotation_angle(jd_tt);
let r_ecef = rot_eci_to_ecef(theta_g, r_eci);
let mut acc: BTreeMap<(usize, usize), (f64, f64)> = BTreeMap::new();
for d in solid_earth_tide_step1(jd_tt) {
let e = acc.entry((d.n, d.m)).or_insert((0.0, 0.0));
e.0 += d.dc;
e.1 += d.ds;
}
if let Some(e) = acc.get_mut(&(2, 0)) {
e.0 -= permanent_tide_c20();
}
for d in ocean_tide(jd_tt) {
let e = acc.entry((d.n, d.m)).or_insert((0.0, 0.0));
e.0 += d.dc;
e.1 += d.ds;
}
for d in atmospheric_tide(jd_tt) {
let e = acc.entry((d.n, d.m)).or_insert((0.0, 0.0));
e.0 += d.dc;
e.1 += d.ds;
}
let mut field = SphericalHarmonicField::zeros(EGM2008_GM, EGM2008_RE, 4);
for ((n, m), (dc, ds)) in acc {
if n >= 2 {
field.set(n, m, dc, ds);
}
}
let a_ecef = field.acceleration(r_ecef);
rot_ecef_to_eci(theta_g, a_ecef)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn doodson_k1_phase_is_theta_g_plus_pi() {
let jd = 2_453_736.5;
let got = doodson_phase(&[1, 1, 0, 0, 0, 0], &doodson_args(jd));
let want = earth_rotation_angle(jd) + std::f64::consts::PI;
let two_pi = 2.0 * std::f64::consts::PI;
let d = (got - want).rem_euclid(two_pi);
let d = d.min(two_pi - d);
assert!(
d < 1e-9,
"K1 Doodson phase {got} vs θ_g+π {want} (wrapped diff {d})"
);
}
#[test]
fn fes2004_m2_22_matches_source() {
let &(_, _, _, cp, sp, cm, sm) = FES2004
.iter()
.find(|&&(mult, n, m, ..)| mult == [2, 0, 0, 0, 0, 0] && n == 2 && m == 2)
.expect("M2 (2,2) present");
assert_eq!((cp, sp, cm, sm), (-39.36214, 46.75729, 9.57270, 5.24459));
}
#[test]
fn tidal_acceleration_is_physical_at_leo() {
let r = [7.0e6, 1.0e6, 2.0e6];
let a = tidal_acceleration(r, 2_453_736.5);
let mag = (a[0] * a[0] + a[1] * a[1] + a[2] * a[2]).sqrt();
assert!(mag.is_finite(), "tidal acceleration must be finite");
assert!(
(1e-9..1e-6).contains(&mag),
"tidal accel {mag:e} m/s² outside the physical 1e-9..1e-6 band"
);
let rn = (r[0] * r[0] + r[1] * r[1] + r[2] * r[2]).sqrt();
let two_body = crate::forces::MU_EARTH / (rn * rn);
assert!(
mag < 1e-5 * two_body,
"tide should be << two-body ({mag:e} vs {two_body:e})"
);
}
#[test]
fn s2_air_tide_22_matches_ray2001() {
let &(_, _, dp, psp, dm, psm) = S2_AIR
.iter()
.find(|&&(n, m, ..)| n == 2 && m == 2)
.expect("S2 air (2,2) present");
assert_eq!((dp, psp, dm, psm), (365.07, 292.85, 6.21, 292.80));
}
}