use crate::consts;
use crate::mathtypes::*;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum TideModel {
None,
#[default]
SolidStep1,
SolidFull,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct TideDeltas {
pub dc: [[f64; 5]; 5],
pub ds: [[f64; 5]; 5],
}
const K_RE: [[f64; 4]; 4] = [
[0.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 0.0],
[0.30190, 0.29830, 0.30102, 0.0],
[0.09300, 0.09300, 0.09300, 0.09400],
];
const K_IM: [[f64; 4]; 4] = [
[0.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 0.0],
[0.0, -0.00144, -0.00130, 0.0],
[0.0, 0.0, 0.0, 0.0],
];
const K_PLUS: [f64; 3] = [-0.00089, -0.00080, -0.00057];
const N20: f64 = 2.2360679774997896; const N21: f64 = 1.2909944487358056; const N22: f64 = 0.6454972243679028; const N30: f64 = 2.6457513110645907; const N31: f64 = 1.0801234497346434; const N32: f64 = 0.3415650255319866; const N33: f64 = 0.13943375672974065; const N40: f64 = 3.0; const N41: f64 = 0.9486832980505138; const N42: f64 = 0.22360679774997896;
pub fn solid_tide_deltas(
sun_itrf: &Vector3,
moon_itrf: &Vector3,
_time: &crate::Instant,
model: TideModel,
) -> TideDeltas {
let mut deltas = TideDeltas::default();
if model == TideModel::None {
return deltas;
}
let r_e = consts::EARTH_RADIUS;
let mu_e = consts::MU_EARTH;
for (body_pos, body_mu) in [(sun_itrf, consts::MU_SUN), (moon_itrf, consts::MU_MOON)] {
accumulate_step1(body_pos, body_mu, mu_e, r_e, &mut deltas);
}
deltas
}
fn accumulate_step1(
body_pos: &Vector3,
body_mu: f64,
mu_e: f64,
r_e: f64,
deltas: &mut TideDeltas,
) {
let r_body = body_pos.norm();
let mu_ratio = body_mu / mu_e;
let sin_phi = body_pos[2] / r_body;
let cos_phi = (1.0 - sin_phi * sin_phi).max(0.0).sqrt();
let u = sin_phi;
let u2 = u * u;
let c2 = cos_phi * cos_phi;
let c3 = c2 * cos_phi;
let rxy = (body_pos[0] * body_pos[0] + body_pos[1] * body_pos[1]).sqrt();
let (cos_l, sin_l) = if rxy > 0.0 {
(body_pos[0] / rxy, body_pos[1] / rxy)
} else {
(1.0, 0.0)
};
let mut cml = [0.0_f64; 4];
let mut sml = [0.0_f64; 4];
cml[0] = 1.0;
cml[1] = cos_l;
sml[1] = sin_l;
cml[2] = 2.0 * cos_l * cml[1] - cml[0];
sml[2] = 2.0 * cos_l * sml[1] - sml[0];
cml[3] = 2.0 * cos_l * cml[2] - cml[1];
sml[3] = 2.0 * cos_l * sml[2] - sml[1];
let p_bar: [[f64; 4]; 4] = [
[0.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 0.0],
[
N20 * 0.5 * (3.0 * u2 - 1.0),
N21 * 3.0 * u * cos_phi,
N22 * 3.0 * c2,
0.0,
],
[
N30 * 0.5 * (5.0 * u2 * u - 3.0 * u),
N31 * 1.5 * (5.0 * u2 - 1.0) * cos_phi,
N32 * 15.0 * u * c2,
N33 * 15.0 * c3,
],
];
let re_over_r = r_e / r_body;
let mut re_over_r_np1 = re_over_r * re_over_r * re_over_r; for n in 2..=3 {
let inv_2np1 = 1.0 / (2 * n + 1) as f64;
for m in 0..=n {
let a = mu_ratio * re_over_r_np1 * p_bar[n][m] * inv_2np1;
let k_re = K_RE[n][m];
let k_im = K_IM[n][m];
deltas.dc[n][m] += a * (k_re * cml[m] + k_im * sml[m]);
deltas.ds[n][m] += a * (k_re * sml[m] - k_im * cml[m]);
}
re_over_r_np1 *= re_over_r;
}
let re_over_r_3 = (r_e / r_body).powi(3);
let factor4 = mu_ratio * re_over_r_3 / 5.0;
for m in 0..=2 {
let a = factor4 * p_bar[2][m] * K_PLUS[m];
deltas.dc[4][m] += a * cml[m];
deltas.ds[4][m] += a * sml[m];
}
}
pub fn tide_accel(pos_itrf: &Vector3, deltas: &TideDeltas, mu_e: f64, r_e: f64) -> Vector3 {
let n_fac: [[f64; 5]; 5] = [
[0.0, 0.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 0.0, 0.0],
[N20, N21, N22, 0.0, 0.0],
[N30, N31, N32, N33, 0.0],
[N40, N41, N42, 0.0, 0.0],
];
let mut c = [[0.0_f64; 5]; 5];
let mut s = [[0.0_f64; 5]; 5];
for n in 2..=4 {
let m_max = if n == 4 { 2 } else { n };
for m in 0..=m_max {
c[n][m] = n_fac[n][m] * deltas.dc[n][m];
s[n][m] = n_fac[n][m] * deltas.ds[n][m];
}
}
let (v, w) = cunningham_vw(pos_itrf, r_e);
let mut ax = 0.0;
let mut ay = 0.0;
let mut az = 0.0;
for n in 2..=4 {
let cn0 = c[n][0];
ax += -cn0 * v[n + 1][1];
ay += -cn0 * w[n + 1][1];
az += -((n + 1) as f64) * cn0 * v[n + 1][0];
let m_max = if n == 4 { 2 } else { n };
for m in 1..=m_max {
let cnm = c[n][m];
let snm = s[n][m];
let fac_a = ((n - m + 2) * (n - m + 1)) as f64;
let fac_z = (n - m + 1) as f64;
let vnp1mp1 = v[n + 1][m + 1];
let wnp1mp1 = w[n + 1][m + 1];
let vnp1mm1 = v[n + 1][m - 1];
let wnp1mm1 = w[n + 1][m - 1];
ax += 0.5 * (-cnm * vnp1mp1 - snm * wnp1mp1 + fac_a * (cnm * vnp1mm1 + snm * wnp1mm1));
ay += 0.5 * (-cnm * wnp1mp1 + snm * vnp1mp1 + fac_a * (-cnm * wnp1mm1 + snm * vnp1mm1));
az += fac_z * (-cnm * v[n + 1][m] - snm * w[n + 1][m]);
}
}
let scale = mu_e / (r_e * r_e);
numeris::vector![ax * scale, ay * scale, az * scale]
}
fn cunningham_vw(pos: &Vector3, r_e: f64) -> ([[f64; 6]; 6], [[f64; 6]; 6]) {
let r2 = pos.norm_squared();
let r = r2.sqrt();
let inv_r2 = 1.0 / r2;
let xf = pos[0] * r_e * inv_r2;
let yf = pos[1] * r_e * inv_r2;
let zf = pos[2] * r_e * inv_r2;
let rf = r_e * r_e * inv_r2;
let mut v = [[0.0_f64; 6]; 6];
let mut w = [[0.0_f64; 6]; 6];
v[0][0] = r_e / r;
for m in 1..=5 {
let mm1 = m - 1;
let two = (2 * m - 1) as f64;
v[m][m] = two * (xf * v[mm1][mm1] - yf * w[mm1][mm1]);
w[m][m] = two * (xf * w[mm1][mm1] + yf * v[mm1][mm1]);
}
for m in 0..=5 {
for n in (m + 1)..=5 {
let nm = (n - m) as f64;
let f1 = ((2 * n - 1) as f64) / nm;
let (vnm2, wnm2) = if n >= m + 2 {
(v[n - 2][m], w[n - 2][m])
} else {
(0.0, 0.0)
};
let f2 = if n >= m + 2 {
((n + m - 1) as f64) / nm
} else {
0.0
};
v[n][m] = f1 * zf * v[n - 1][m] - f2 * rf * vnm2;
w[n][m] = f1 * zf * w[n - 1][m] - f2 * rf * wnm2;
}
}
(v, w)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Instant;
fn epoch() -> Instant {
Instant::from_datetime(2020, 1, 1, 0, 0, 0.0).unwrap()
}
#[test]
fn zero_deltas_give_zero_accel() {
let pos = numeris::vector![consts::GEO_R, 0.0, 0.0];
let deltas = TideDeltas::default();
let a = tide_accel(&pos, &deltas, consts::MU_EARTH, consts::EARTH_RADIUS);
assert!(a.norm() < 1e-20, "tide_accel of zero deltas = {:?}", a);
}
#[test]
fn none_model_yields_empty_deltas() {
let sun = numeris::vector![1.49e11, 0.0, 0.0];
let moon = numeris::vector![3.84e8, 0.0, 0.0];
let deltas = solid_tide_deltas(&sun, &moon, &epoch(), TideModel::None);
for n in 0..5 {
for m in 0..5 {
assert_eq!(deltas.dc[n][m], 0.0);
assert_eq!(deltas.ds[n][m], 0.0);
}
}
}
#[test]
fn body_over_pole_excites_only_zonal_terms() {
let sun = numeris::vector![0.0, 0.0, 1.49e11];
let moon = numeris::vector![0.0, 0.0, 3.84e8];
let deltas = solid_tide_deltas(&sun, &moon, &epoch(), TideModel::SolidStep1);
for n in 2..=4 {
for m in 1..=n {
assert!(
deltas.dc[n][m].abs() < 1e-25,
"ΔC̄[{}][{}] = {:e} (expected 0)",
n,
m,
deltas.dc[n][m]
);
assert!(
deltas.ds[n][m].abs() < 1e-25,
"ΔS̄[{}][{}] = {:e} (expected 0)",
n,
m,
deltas.ds[n][m]
);
}
}
assert!(deltas.dc[2][0] > 0.0);
}
#[test]
fn equator_x_body_gives_negative_dc20() {
let moon = numeris::vector![3.84e8, 0.0, 0.0];
let no_sun = numeris::vector![1e30, 0.0, 0.0]; let deltas = solid_tide_deltas(&no_sun, &moon, &epoch(), TideModel::SolidStep1);
assert!(
deltas.dc[2][0] < 0.0,
"ΔC̄_20 from equatorial body = {:e}, expected < 0",
deltas.dc[2][0]
);
}
#[test]
fn tide_accel_magnitude_at_geo_is_reasonable() {
let sun = numeris::vector![1.0e11, 1.0e11, 0.0]; let moon = numeris::vector![3.0e8, 2.0e8, 5.0e7]; let deltas = solid_tide_deltas(&sun, &moon, &epoch(), TideModel::SolidStep1);
let pos = numeris::vector![consts::GEO_R, 0.0, 0.0];
let a = tide_accel(&pos, &deltas, consts::MU_EARTH, consts::EARTH_RADIUS);
let mag = a.norm();
assert!(
(1e-11..1e-6).contains(&mag),
"tide accel at GEO = {:e}, expected ~1e-9 to 1e-7",
mag
);
}
#[test]
fn cunningham_v00_is_re_over_r() {
let pos = numeris::vector![1.0e7, 2.0e7, 3.0e7];
let r = pos.norm();
let (v, _) = cunningham_vw(&pos, consts::EARTH_RADIUS);
let expected = consts::EARTH_RADIUS / r;
assert!((v[0][0] - expected).abs() / expected < 1e-15);
}
#[test]
fn solid_full_currently_matches_step1() {
let sun = numeris::vector![1.4e11, 5e10, 2e10];
let moon = numeris::vector![3.2e8, 1.5e8, 4e7];
let d1 = solid_tide_deltas(&sun, &moon, &epoch(), TideModel::SolidStep1);
let d2 = solid_tide_deltas(&sun, &moon, &epoch(), TideModel::SolidFull);
for n in 2..=4 {
for m in 0..=n.min(4) {
assert_eq!(d1.dc[n][m], d2.dc[n][m]);
assert_eq!(d1.ds[n][m], d2.ds[n][m]);
}
}
}
}