use core::f64::consts::TAU;
use geopotential::geocentric;
use metrology::WGS84_A;
pub const PERMANENT_A: f64 = -2.9166;
pub fn delta(n: u32, h: f64, k: f64) -> f64 {
let n = f64::from(n);
1.0 + 2.0 * h / n - (n + 1.0) * k / n
}
pub fn gamma(h: f64, k: f64) -> f64 {
1.0 + k - h
}
pub fn longman(lat: f64, lon: f64, h: f64, epoch: f64) -> (f64, f64, f64) {
let (mu, moon, sun) = (6.673e-8, 7.3537e25, 1.993e33);
let (e, m, c, c1, a) = (0.05490, 0.074804, 3.84402e10, 1.495e13, 6.378270e8);
let (i, omega): (f64, f64) = (0.08979719, 23.452f64.to_radians());
let tc = (epoch - 2000.0) / 100.0 + 1.0;
let day = (epoch - 2000.0) * 365.25;
let t = TAU * (day - day.floor()) + lon;
let (tc2, tc3) = (tc * tc, tc * tc * tc);
let s = 4.72000889397 + 8399.70927456 * tc + 3.45575191895e-05 * tc2 + 3.49065850399e-08 * tc3;
let p = 5.83515162814 + 71.0180412089 * tc + 0.000180108282532 * tc2 + 1.74532925199e-07 * tc3;
let hs = 4.88162798259 + 628.331950894 * tc + 5.23598775598e-06 * tc2;
let node = 4.52360161181 - 33.757146295 * tc + 3.6264063347e-05 * tc2 + 3.39369576777e-08 * tc3;
let incl = (omega.cos() * i.cos() - omega.sin() * i.sin() * node.cos()).acos();
let nu = (i.sin() * node.sin() / incl.sin()).asin();
let chi = t + hs - nu;
let (cos_alpha, sin_alpha) = (node.cos() * nu.cos() + node.sin() * nu.sin() * omega.cos(), omega.sin() * node.sin() / incl.sin());
let sigma = s - (node - 2.0 * (sin_alpha / (1.0 + cos_alpha)).atan());
let l = sigma + 2.0 * e * (s - p).sin() + 1.25 * e * e * (2.0 * (s - p)).sin()
+ 3.75 * m * e * (s - 2.0 * hs + p).sin() + 11.0 / 8.0 * m * m * (2.0 * (s - hs)).sin();
let p1 = 4.90822941839 + 0.0300025492114 * tc + 7.85398163397e-06 * tc2 + 5.3329504922e-08 * tc3;
let e1 = 0.01675104 - 0.00004180 * tc - 0.000000126 * tc2;
let chi1 = t + hs;
let l1 = hs + 2.0 * e1 * (hs - p1).sin();
let (sl, cl) = lat.sin_cos();
let cos_theta = sl * incl.sin() * l.sin() + cl * ((0.5 * incl).cos().powi(2) * (l - chi).cos() + (0.5 * incl).sin().powi(2) * (l + chi).cos());
let cos_phi = sl * omega.sin() * l1.sin() + cl * ((0.5 * omega).cos().powi(2) * (l1 - chi1).cos() + (0.5 * omega).sin().powi(2) * (l1 + chi1).cos());
let r = (1.0 / (1.0 + 0.006738 * sl * sl)).sqrt() * a + h * 100.0;
let (ap, ap1) = (1.0 / (c * (1.0 - e * e)), 1.0 / (c1 * (1.0 - e1 * e1)));
let d = 1.0 / (1.0 / c + ap * e * (s - p).cos() + ap * e * e * (2.0 * (s - p)).cos()
+ 15.0 / 8.0 * ap * m * e * (s - 2.0 * hs + p).cos() + ap * m * m * (2.0 * (s - hs)).cos());
let big_d = 1.0 / (1.0 / c1 + ap1 * e1 * (hs - p1).cos());
let gm = mu * moon * r / (d * d * d) * (3.0 * cos_theta * cos_theta - 1.0)
+ 1.5 * mu * moon * r * r / (d * d * d * d) * (5.0 * cos_theta.powi(3) - 3.0 * cos_theta);
let gs = mu * sun * r / (big_d * big_d * big_d) * (3.0 * cos_phi * cos_phi - 1.0);
let k = 1e-2 * delta(2, 0.612, 0.303);
(gm * k, gs * k, (gm + gs) * k)
}
pub fn permanent(lat: f64, h: f64) -> (f64, f64) {
let (r, psi) = geocentric(lat, h);
let (p, z) = (r * psi.cos(), r * psi.sin());
let k = PERMANENT_A / (3.0 * WGS84_A * WGS84_A);
(k * (2.0 * z * z - p * p), 2.0 * k * (p * lat.cos() - 2.0 * z * lat.sin()))
}
#[cfg(test)]
mod tests {
use super::*;
use geopotential::normal::GRS80;
use geopotential::{Coefficients, TideSystem, julian_epoch};
use metrology::GM_EARTH;
const MICROGAL: f64 = crate::local::MICROGAL;
#[test]
fn longmans_tide() {
let (lat, lon) = (40.7914f64.to_radians(), 282.1414f64.to_radians());
let (moon, sun, total) = longman(lat, lon, 370.0, julian_epoch(2015, 4, 23, 0, 0));
for (got, want) in [(moon, 0.0324029651226), (sun, -0.0288682178454), (total, 0.00353474727722)] {
assert!((got / 1e-5 - want).abs() < 1e-10, "{} against {want}", got / 1e-5);
}
}
#[test]
fn averaged_over_the_nodal_cycle_it_is_the_permanent_tide() {
let step = 1.37 / 24.0 / 365.25; let n = (18.61 / step) as usize;
for deg in [0.0, 45.0, 90.0] {
let lat = f64::to_radians(deg);
let mean = (0..n).map(|k| longman(lat, 0.0, 0.0, 2000.0 + k as f64 * step).2).sum::<f64>() / n as f64;
let want = -delta(2, 0.612, 0.303) * permanent(lat, 0.0).1;
assert!((mean - want).abs() < 0.25 * MICROGAL, "{deg}°: {} against {} µGal", mean / MICROGAL, want / MICROGAL);
}
}
#[test]
fn love_number_factors() {
assert!((delta(2, 0.612, 0.303) - 1.1575).abs() < 1e-15);
assert!((delta(2, 0.6078, 0.30190) - 1.15495).abs() < 1e-14);
assert!((delta(2, 0.6078, 0.30190) - 1.16).abs() < 0.01); assert!((delta(3, 0.292, 0.093) - 1.070_666_666_666_667).abs() < 1e-15);
assert!((gamma(0.6078, 0.30190) - 0.6941).abs() < 1e-12 && (gamma(0.6078, 0.30190) - 0.69).abs() < 0.005);
assert_eq!((delta(2, 0.0, 0.0), gamma(0.0, 0.0), gamma(1.25, 0.25)), (1.0, 1.0, 0.0));
}
#[test]
fn makinens_permanent_tide() {
let a_iers = 1.5 * -0.31460 * (5.0 / (4.0 * core::f64::consts::PI)).sqrt() * 9.79828685 * (WGS84_A / 6378136.55f64).powi(2);
assert!((a_iers - PERMANENT_A).abs() < 5e-5, "{a_iers}");
for deg in (0..=90).step_by(5) {
let lat = f64::to_radians(f64::from(deg));
let s2 = lat.sin().powi(2);
let (w, g) = permanent(lat, 0.0);
assert!((w - (0.9722 - 2.8841 * s2 - 0.0195 * s2 * s2)).abs() < 5e-5, "{deg}°: W_T {w}");
assert!((g / MICROGAL - (-30.49 + 90.95 * s2 + 0.31 * s2 * s2)).abs() < 0.005, "{deg}°: g_T {}", g / MICROGAL);
assert!((w / GRS80.gamma(lat) * 1e3 - (99.40 - 295.41 * s2 - 0.42 * s2 * s2)).abs() < 0.005, "{deg}°: H_T");
}
assert!((permanent(0.0, 0.0).1 / MICROGAL + 30.485).abs() < 1e-3);
assert!((permanent(0.0, 0.0).0 / GRS80.gamma(0.0) - 0.0994).abs() < 1e-4);
let lat = 45f64.to_radians();
assert!((permanent(lat, 1e3).0 / permanent(lat, 0.0).0 - (1.0 + 2e3 / WGS84_A)).abs() < 5e-6);
}
#[test]
fn the_mean_tide_c20_is_right_only_on_its_sphere() {
let mut zero = Coefficients::zeros(GM_EARTH, WGS84_A, 2);
zero.tide_system = TideSystem::ZeroTide;
let mean = zero.to_tide_system(TideSystem::MeanTide).unwrap();
let v = |c: &Coefficients, r: f64| c.partials(r, 0.0, 0.0).v;
let added = v(&mean, WGS84_A) - v(&zero, WGS84_A);
let (w, g) = permanent(0.0, 0.0);
assert!((added / w - 1.0).abs() < 2e-4, "{}", added / w);
let dr = 1.0;
let pull = -((v(&mean, WGS84_A + dr) - v(&zero, WGS84_A + dr)) - added) / dr;
assert!((pull / g + 1.5).abs() < 1e-3 && (pull / MICROGAL - 45.7).abs() < 0.1, "{}", pull / MICROGAL);
}
}