use core::f64::consts::FRAC_PI_4;
use metrology::{EARTH_ROTATION, GM_EARTH, GRS80_F, GRS80_GM, WGS84_A, WGS84_F};
use crate::Coefficients;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct NormalGravity {
pub a: f64,
pub f: f64,
pub gm: f64,
pub omega: f64,
}
pub const GRS80: NormalGravity = NormalGravity { a: WGS84_A, f: GRS80_F, gm: GRS80_GM, omega: EARTH_ROTATION };
pub const WGS84: NormalGravity = NormalGravity { a: WGS84_A, f: WGS84_F, gm: GM_EARTH, omega: EARTH_ROTATION };
impl NormalGravity {
pub fn b(&self) -> f64 {
self.a * (1.0 - self.f)
}
pub fn e2(&self) -> f64 {
self.f * (2.0 - self.f)
}
fn e_prime(&self) -> f64 {
(self.a * self.a - self.b() * self.b()).sqrt() / self.b()
}
pub fn m(&self) -> f64 {
self.omega * self.omega * self.a * self.a * self.b() / self.gm
}
fn q(&self) -> (f64, f64) {
let e = self.e_prime();
let at = e.atan();
(0.5 * ((1.0 + 3.0 / (e * e)) * at - 3.0 / e), 3.0 * (1.0 + 1.0 / (e * e)) * (1.0 - at / e) - 1.0)
}
pub fn gamma_equator(&self) -> f64 {
let (q0, q0p) = self.q();
let m = self.m();
self.gm / (self.a * self.b()) * (1.0 - m - m * self.e_prime() * q0p / (6.0 * q0))
}
pub fn gamma_pole(&self) -> f64 {
let (q0, q0p) = self.q();
self.gm / (self.a * self.a) * (1.0 + self.m() * self.e_prime() * q0p / (3.0 * q0))
}
pub fn k(&self) -> f64 {
self.b() * self.gamma_pole() / (self.a * self.gamma_equator()) - 1.0
}
pub fn u0(&self) -> f64 {
let e = self.e_prime();
self.gm / (e * self.b()) * e.atan() + self.omega * self.omega * self.a * self.a / 3.0
}
pub fn j2(&self) -> f64 {
let (q0, _) = self.q();
self.e2() / 3.0 * (1.0 - 2.0 * self.m() * self.e_prime() / (15.0 * q0))
}
pub fn zonal(&self, n: usize) -> f64 {
let (e2, nf) = (self.e2(), n as f64);
let sign = if n % 2 == 1 { 1.0 } else { -1.0 };
sign * 3.0 * e2.powi(n as i32) / ((2.0 * nf + 1.0) * (2.0 * nf + 3.0)) * (1.0 - nf + 5.0 * nf * self.j2() / e2)
}
pub fn coefficients(&self, nmax: usize) -> Coefficients {
let mut c = Coefficients::zeros(self.gm, self.a, nmax);
c.set(0, 0, 1.0, 0.0);
for n in 1..=nmax / 2 {
c.set(2 * n, 0, -self.zonal(n) / (4.0 * n as f64 + 1.0).sqrt(), 0.0);
}
c
}
pub fn gamma(&self, lat: f64) -> f64 {
let s2 = lat.sin().powi(2);
self.gamma_equator() * (1.0 + self.k() * s2) / (1.0 - self.e2() * s2).sqrt()
}
pub fn gamma_at(&self, lat: f64, h: f64) -> f64 {
let x = h / self.a;
self.gamma(lat) * (1.0 - 2.0 * self.height_coefficient(lat) * x + 3.0 * x * x)
}
pub fn mean_gamma(&self, lat: f64, h: f64) -> f64 {
let x = h / self.a;
self.gamma(lat) * (1.0 - self.height_coefficient(lat) * x + x * x)
}
fn height_coefficient(&self, lat: f64) -> f64 {
1.0 + self.f + self.m() - 2.0 * self.f * lat.sin().powi(2)
}
pub fn geocentric(&self, lat: f64, h: f64) -> (f64, f64) {
let e2 = self.e2();
let (s, c) = lat.sin_cos();
let n = self.a / (1.0 - e2 * s * s).sqrt();
let (p, z) = ((n + h) * c, (n * (1.0 - e2) + h) * s);
(p.hypot(z), z.atan2(p))
}
pub fn dynamic_height(&self, c: f64) -> f64 {
c / self.gamma(FRAC_PI_4)
}
pub fn normal_height(&self, c: f64, lat: f64) -> f64 {
let mut h = c / self.gamma(lat);
for _ in 0..8 {
let next = c / self.mean_gamma(lat, h);
if (next - h).abs() < 1e-12 {
return next;
}
h = next;
}
h
}
}
pub fn bruns(t: f64, gamma: f64, w0: f64, u0: f64) -> f64 {
(t - (w0 - u0)) / gamma
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Field;
use metrology::W0_IHRS;
fn close(a: f64, b: f64, tol: f64) -> bool {
(a - b).abs() <= tol
}
#[test]
fn grs80_derived_constants() {
assert!(close(GRS80.e2(), 0.006_694_380_022_90, 5e-15) && close(GRS80.m(), 0.003_449_786_003_08, 5e-15));
assert!(close(GRS80.gamma_equator(), 9.780_326_771_5, 5e-11) && close(GRS80.gamma_pole(), 9.832_186_368_5, 5e-11));
assert!(close(GRS80.k(), 0.001_931_851_353, 5e-13) && close(GRS80.u0(), 62_636_860.850, 5e-4));
assert!(close(GRS80.j2(), 108_263e-8, 5e-16));
assert!(close(GRS80.zonal(2), -0.000_002_370_912_22, 5e-15) && close(GRS80.zonal(3), 0.000_000_006_083_47, 5e-15));
assert!(close(GRS80.zonal(4), -0.000_000_000_014_27, 5e-15));
assert!(close(GRS80.gamma(FRAC_PI_4), 9.806_199_203, 5e-10));
assert!(close(GRS80.gamma_pole(), GRS80.gamma(core::f64::consts::FRAC_PI_2), 1e-14));
}
#[test]
fn wgs84_derived_constants() {
assert!(close(WGS84.e2(), 6.694_379_990_14e-3, 5e-15) && close(WGS84.m(), 0.003_449_786_506_84, 5e-15));
assert!(close(WGS84.gamma_equator(), 9.780_325_335_9, 5e-11) && close(WGS84.gamma_pole(), 9.832_184_937_8, 1e-10));
assert!(close(WGS84.k(), 0.001_931_852_652_41, 5e-15) && close(WGS84.u0(), 62_636_851.714_6, 5e-5));
assert!(close(WGS84.j2(), 1.082_629_821_31e-3, 5e-15));
let wgs84 = WGS84.coefficients(8);
assert!(close(wgs84.c(2, 0), -0.484_166_774_985e-3, 5e-16) && close(wgs84.c(4, 0), 0.790_303_733_511e-6, 5e-18));
assert!(close(wgs84.c(6, 0), -0.168_724_961_151e-8, 5e-20) && close(wgs84.c(8, 0), 0.346_052_468_394e-11, 5e-22));
assert!(close(GRS80.coefficients(2).c(2, 0), -0.484_166_854_896e-3, 5e-16));
}
#[test]
fn the_synthesized_normal_field_is_somiglianas() {
for normal in [GRS80, WGS84] {
let field = normal.coefficients(20);
for deg in [0.0, 15.0, 30.0, 45.0, 60.0, 75.0, 89.9] {
let lat = f64::to_radians(deg);
let (r, psi) = normal.geocentric(lat, 0.0);
let p = field.partials(r, psi, 0.3);
let [n, e, u] = p.gravity_neu(r, psi, normal.omega);
let g = (n * n + e * e + u * u).sqrt();
assert!(close(g, normal.gamma(lat), 1e-10), "{deg}°: {g} against {}", normal.gamma(lat));
assert!(close(p.geopotential(r, psi, normal.omega), normal.u0(), 1e-5), "{deg}°");
}
let (r, psi) = normal.geocentric(0.7, 0.0);
let [n, e, u] = normal.coefficients(8).partials(r, psi, 0.0).gravity_neu(r, psi, normal.omega);
assert!(close((n * n + e * e + u * u).sqrt(), normal.gamma(0.7), 2e-8));
}
}
#[test]
fn gravity_above_the_ellipsoid() {
let lat = FRAC_PI_4;
let gradient = (GRS80.gamma(lat) - GRS80.gamma_at(lat, 1.0)) * 1e5;
assert!(close(gradient, 0.308_55, 1e-5) && close(gradient, 0.3086, 1e-4), "{gradient}");
assert!(close(GRS80.gamma(lat) - GRS80.gamma_at(lat, 1e3), 308.48e-5, 1e-7));
let simpson = (GRS80.gamma(lat) + 4.0 * GRS80.gamma_at(lat, 500.0) + GRS80.gamma_at(lat, 1e3)) / 6.0;
assert!(close(GRS80.mean_gamma(lat, 1e3), simpson, 1e-12));
let (r, psi) = GRS80.geocentric(lat, 100.0);
let (r2, psi2) = crate::geocentric(lat, 100.0);
assert!(close(r, r2, 1e-3) && close(psi, psi2, 1e-10) && (45.0 - psi.to_degrees()) * 60.0 > 11.5);
}
#[test]
fn heights_from_geopotential_numbers() {
let c = 1000.0 * GRS80.gamma(FRAC_PI_4);
assert!(close(GRS80.dynamic_height(c), 1000.0, 1e-9));
let h = GRS80.normal_height(c, FRAC_PI_4);
assert!(close(h * GRS80.mean_gamma(FRAC_PI_4, h), c, 1e-9) && close(h, 1000.157, 1e-3), "{h}");
assert_eq!(GRS80.normal_height(0.0, 0.3), 0.0);
let gamma = GRS80.gamma(0.5);
assert_eq!(bruns(0.0, gamma, GRS80.u0(), GRS80.u0()), 0.0);
assert!(close(bruns(0.0, gamma, GRS80.u0() - 9.8, GRS80.u0()), 1.0, 0.002));
assert!(close(bruns(0.0, GRS80.gamma(0.0), W0_IHRS, GRS80.u0()), 0.762, 1e-3));
}
#[test]
fn deflections_of_the_vertical() {
let (lat, lon) = (0.5, 0.7);
let (r, psi) = GRS80.geocentric(lat, 0.0);
let gamma = GRS80.gamma(lat);
let mut t = Coefficients::zeros(GRS80.gm, GRS80.a, 3);
t.set(3, 0, 1e-6, 0.0);
let (xi, eta) = t.partials(r, psi, lon).deflections(r, psi, gamma);
let h = 1e-5;
let slope = (t.potential(r, psi + h, lon) - t.potential(r, psi - h, lon)) / (2.0 * h);
assert!(close(xi, -slope / (gamma * r), 1e-9 * xi.abs()) && xi.abs() > 1e-7 && eta == 0.0, "{xi} {eta}");
t.set(3, 0, 0.0, 0.0);
t.set(2, 1, 1e-6, 1e-6);
let (_, eta) = t.partials(r, psi, lon).deflections(r, psi, gamma);
let slope = (t.potential(r, psi, lon + h) - t.potential(r, psi, lon - h)) / (2.0 * h);
assert!(close(eta, -slope / (gamma * r * psi.cos()), 1e-9 * eta.abs()) && eta.abs() > 1e-7, "{eta}");
}
}