use crate::coefficients::Coefficients;
use crate::functional::Functional;
use crate::legendre::{Legendre, idx};
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Partials {
pub v: f64,
pub dr: f64,
pub dlat: f64,
pub dlon: f64,
}
impl Partials {
pub fn acceleration_neu(&self, r: f64, lat: f64) -> [f64; 3] {
[self.dlat / r, self.dlon / (r * lat.cos()), self.dr]
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct SecondPartials {
pub first: Partials,
pub drr: f64,
pub dlatlat: f64,
pub dlonlon: f64,
pub drlat: f64,
pub drlon: f64,
pub dlatlon: f64,
}
impl Coefficients {
pub fn second_partials(&self, r: f64, lat: f64, lon: f64) -> SecondPartials {
let nmax = self.max_degree();
let mut leg = Legendre::new(nmax);
leg.compute_with_second_derivative(lat);
let (cos_m, sin_m): (Vec<f64>, Vec<f64>) =
(0..=nmax).map(|m| ((m as f64 * lon).cos(), (m as f64 * lon).sin())).unzip();
let q = self.radius / r;
let mut acc = [0.0f64; 10]; let mut qn = 1.0;
for n in 0..=nmax {
let mut sum = [0.0f64; 6]; for m in 0..=n {
let k = idx(n, m);
let (c, s) = (self.c[k], self.s[k]);
let mf = m as f64;
let even = c * cos_m[m] + s * sin_m[m];
let deven = mf * (s * cos_m[m] - c * sin_m[m]);
sum[0] += leg.p[k] * even;
sum[1] += leg.dp[k] * even;
sum[2] += leg.ddp[k] * even;
sum[3] += leg.p[k] * deven;
sum[4] -= leg.p[k] * mf * mf * even;
sum[5] += leg.dp[k] * deven;
}
let (n1, n12) = ((n + 1) as f64, ((n + 1) * (n + 2)) as f64);
let terms = [sum[0], -n1 * sum[0], sum[1], sum[3], n12 * sum[0], sum[2], sum[4], -n1 * sum[1], -n1 * sum[3], sum[5]];
for (a, t) in acc.iter_mut().zip(terms) {
*a += qn * t;
}
qn *= q;
}
let gmr = self.gm / r;
SecondPartials {
first: Partials { v: gmr * acc[0], dr: gmr / r * acc[1], dlat: gmr * acc[2], dlon: gmr * acc[3] },
drr: gmr / (r * r) * acc[4],
dlatlat: gmr * acc[5],
dlonlon: gmr * acc[6],
drlat: gmr / r * acc[7],
drlon: gmr / r * acc[8],
dlatlon: gmr * acc[9],
}
}
pub fn partials(&self, r: f64, lat: f64, lon: f64) -> Partials {
let nmax = self.max_degree();
let mut leg = Legendre::new(nmax);
leg.compute_with_derivative(lat);
let (cos_m, sin_m): (Vec<f64>, Vec<f64>) =
(0..=nmax).map(|m| ((m as f64 * lon).cos(), (m as f64 * lon).sin())).unzip();
let q = self.radius / r;
let (mut v, mut vr, mut vlat, mut vlon) = (0.0, 0.0, 0.0, 0.0);
let mut qn = 1.0;
for n in 0..=nmax {
let (mut sv, mut slat, mut slon) = (0.0, 0.0, 0.0);
for m in 0..=n {
let k = idx(n, m);
let (c, s) = (self.c[k], self.s[k]);
let even = c * cos_m[m] + s * sin_m[m];
sv += leg.p[k] * even;
slat += leg.dp[k] * even;
slon += leg.p[k] * m as f64 * (s * cos_m[m] - c * sin_m[m]);
}
v += qn * sv;
vr -= (n + 1) as f64 * qn * sv;
vlat += qn * slat;
vlon += qn * slon;
qn *= q;
}
let gmr = self.gm / r;
Partials { v: gmr * v, dr: gmr / r * vr, dlat: gmr * vlat, dlon: gmr * vlon }
}
pub fn grid(&self, functional: &Functional, r: f64, lats: &[f64], lons: &[f64]) -> Vec<f64> {
let nmax = self.max_degree();
let factor: Vec<f64> =
(0..=nmax).map(|n| functional.factor(n, self.gm, self.radius, r)).collect();
let mut leg = Legendre::new(nmax);
let (mut a, mut b) = (vec![0.0; nmax + 1], vec![0.0; nmax + 1]);
let mut out = Vec::with_capacity(lats.len() * lons.len());
for &lat in lats {
leg.compute(lat);
a.fill(0.0);
b.fill(0.0);
for (n, &f) in factor.iter().enumerate() {
for m in 0..=n {
let k = idx(n, m);
let w = f * leg.p[k];
a[m] += w * self.c[k];
b[m] += w * self.s[k];
}
}
out.extend(lons.iter().map(|&lon| {
(0..=nmax)
.map(|m| {
let (s, c) = (m as f64 * lon).sin_cos();
a[m] * c + b[m] * s
})
.sum::<f64>()
}));
}
out
}
}
#[cfg(test)]
mod tests {
use super::*;
const GM: f64 = 3.986004415e14;
const R: f64 = 6378136.3;
fn field(nmax: usize) -> Coefficients {
let mut seed: u64 = 0x2545_F491_4F6C_DD1D;
let mut rand = || {
seed ^= seed << 13;
seed ^= seed >> 7;
seed ^= seed << 17;
(seed >> 11) as f64 / (1u64 << 53) as f64 * 2.0 - 1.0
};
let mut f = Coefficients::zeros(GM, R, nmax);
f.set(0, 0, 1.0, 0.0);
for n in 2..=nmax {
let kaula = 1e-5 / (n * n) as f64;
for m in 0..=n {
let s = if m == 0 { 0.0 } else { kaula * rand() };
f.set(n, m, kaula * rand(), s);
}
}
f
}
#[test]
fn monopole_is_gm_over_r() {
let mut f = Coefficients::zeros(GM, R, 0);
f.set(0, 0, 1.0, 0.0);
let r = 7.0e6;
let p = f.partials(r, 0.4, 1.2);
assert!((p.v - GM / r).abs() < 1e-8);
assert!((p.dr + GM / (r * r)).abs() < 1e-14);
assert_eq!((p.dlat, p.dlon), (0.0, 0.0));
}
#[test]
fn partials_match_finite_differences() {
let mut f = field(30);
f.set(0, 0, 0.0, 0.0); let (r, lat, lon) = (6.9e6, 0.7, -2.1);
let p = f.partials(r, lat, lon);
let v = |r, lat, lon| f.partials(r, lat, lon).v;
let (hr, ha) = (1.0, 1e-6);
let dr = (v(r + hr, lat, lon) - v(r - hr, lat, lon)) / (2.0 * hr);
let dlat = (v(r, lat + ha, lon) - v(r, lat - ha, lon)) / (2.0 * ha);
let dlon = (v(r, lat, lon + ha) - v(r, lat, lon - ha)) / (2.0 * ha);
assert!((p.dr - dr).abs() < 1e-8 * p.dr.abs(), "{} vs {dr}", p.dr);
assert!((p.dlat - dlat).abs() < 1e-5 * p.dlat.abs(), "{} vs {dlat}", p.dlat);
assert!((p.dlon - dlon).abs() < 1e-5 * p.dlon.abs(), "{} vs {dlon}", p.dlon);
}
#[test]
fn laplace_trace_vanishes() {
let f = field(40);
let grad = |x: [f64; 3]| {
let r = (x[0] * x[0] + x[1] * x[1] + x[2] * x[2]).sqrt();
let (lat, lon) = ((x[2] / r).asin(), x[1].atan2(x[0]));
let [n, e, u] = f.partials(r, lat, lon).acceleration_neu(r, lat);
let ((sp, cp), (sl, cl)) = (lat.sin_cos(), lon.sin_cos());
[
-sp * cl * n - sl * e + cp * cl * u,
-sp * sl * n + cl * e + cp * sl * u,
cp * n + sp * u,
]
};
let x0 = [3.1e6, -4.4e6, 4.0e6];
let h = 1.0;
let mut trace = 0.0;
let mut scale = 0.0f64;
for i in 0..3 {
let (mut hi, mut lo) = (x0, x0);
hi[i] += h;
lo[i] -= h;
let d = (grad(hi)[i] - grad(lo)[i]) / (2.0 * h);
trace += d;
scale = scale.max(d.abs());
}
assert!(trace.abs() < 1e-7 * scale, "trace {trace:e} vs diagonal {scale:e}");
}
#[test]
fn grid_functionals_match_partials() {
let mut f = field(25);
f.set(0, 0, 0.0, 0.0); let r = 6.6e6;
let (lats, lons) = ([-1.2, 0.1, 1.5], [0.0, 2.0, 4.5, 6.2]);
let grid = |func| f.grid(&func, r, &lats, &lons);
let (pot, geoid, dist, anom) = (
grid(Functional::Potential),
grid(Functional::GeoidHeight),
grid(Functional::GravityDisturbance),
grid(Functional::GravityAnomaly),
);
let max_abs = |v: &[f64]| v.iter().fold(0.0f64, |a, x| a.max(x.abs()));
let close = |a: f64, b: f64, grid: &[f64]| (a - b).abs() <= 1e-12 * max_abs(grid);
for (i, &lat) in lats.iter().enumerate() {
for (j, &lon) in lons.iter().enumerate() {
let k = i * lons.len() + j;
let p = f.partials(r, lat, lon);
assert!(close(pot[k], p.v, &pot));
assert!(close(geoid[k], p.v / (GM / (r * r)), &geoid));
assert!(close(dist[k], -p.dr, &dist));
assert!(close(anom[k], -p.dr - 2.0 * p.v / r, &anom));
}
}
let h = 1.0;
let up = f.grid(&Functional::GravityDisturbance, r + h, &lats, &lons);
let down = f.grid(&Functional::GravityDisturbance, r - h, &lats, &lons);
let trr = grid(Functional::RadialGradient);
for k in 0..trr.len() {
let fd = -(up[k] - down[k]) / (2.0 * h);
assert!((trr[k] - fd).abs() < 1e-6 * max_abs(&trr), "{} vs {fd}", trr[k]);
}
}
#[test]
fn second_partials_match_differences_and_laplace() {
let mut f = field(25);
f.set(0, 0, 0.0, 0.0);
for (r, lat, lon) in [(6.9e6, 0.6, -1.1), (7.2e6, -1.3, 2.9), (6.7e6, 1.569, 0.4)] {
let sp = f.second_partials(r, lat, lon);
let (a, b) = (sp.first, f.partials(r, lat, lon));
for (x, y) in [(a.v, b.v), (a.dr, b.dr), (a.dlat, b.dlat), (a.dlon, b.dlon)] {
assert!((x - y).abs() <= 1e-13 * y.abs().max(1e-30), "{x} vs {y}"); }
let (hr, ha) = (1.0, 1e-6);
let p = |r, lat, lon| f.partials(r, lat, lon);
let close = |a: f64, b: f64| (a - b).abs() <= 1e-5 * (a.abs() + b.abs() + 1e-30);
assert!(close(sp.drr, (p(r + hr, lat, lon).dr - p(r - hr, lat, lon).dr) / (2.0 * hr)));
assert!(close(sp.dlatlat, (p(r, lat + ha, lon).dlat - p(r, lat - ha, lon).dlat) / (2.0 * ha)));
assert!(close(sp.dlonlon, (p(r, lat, lon + ha).dlon - p(r, lat, lon - ha).dlon) / (2.0 * ha)));
assert!(close(sp.drlat, (p(r, lat + ha, lon).dr - p(r, lat - ha, lon).dr) / (2.0 * ha)));
assert!(close(sp.drlon, (p(r, lat, lon + ha).dr - p(r, lat, lon - ha).dr) / (2.0 * ha)));
assert!(close(sp.dlatlon, (p(r, lat, lon + ha).dlat - p(r, lat, lon - ha).dlat) / (2.0 * ha)));
let lap = sp.drr + 2.0 * sp.first.dr / r
+ (sp.dlatlat - lat.tan() * sp.first.dlat + sp.dlonlon / lat.cos().powi(2)) / (r * r);
assert!(lap.abs() < 1e-9 * sp.drr.abs(), "Laplacian {lap:e} vs {:e}", sp.drr);
}
}
#[test]
fn rescaling_leaves_the_field_unchanged() {
let f = field(20);
let g = f.rescaled(3.986004418e14, 6378137.0);
let (a, b) = (f.partials(7.0e6, 0.3, 1.1), g.partials(7.0e6, 0.3, 1.1));
assert!((a.v - b.v).abs() < 1e-14 * a.v.abs() && (a.dlat - b.dlat).abs() < 1e-12 * a.dlat.abs());
}
#[test]
fn water_height_degree_two() {
let love = [0.0, 0.027, -0.303];
let mut d = Coefficients::zeros(GM, R, 2);
d.set(2, 0, 1e-10, 0.0);
let ewh = d.grid(&Functional::WaterHeight { love: &love }, R, &[core::f64::consts::FRAC_PI_2], &[0.0])[0];
let expect = R * 5517.0 / 3000.0 * 5.0 / 0.697 * 1e-10 * 5f64.sqrt();
assert!((ewh - expect).abs() < 1e-15 && (ewh - 0.0188).abs() < 1e-4, "{ewh}");
}
}