use super::attitude::Quaternion;
use crate::frames::{wgs84_e2, Geodetic, Vec3, WGS84_A, WGS84_F};
pub const OMEGA_IE: f64 = 7.292_115e-5;
const GAMMA_E: f64 = 9.780_325_335_9;
const SOMIGLIANA_K: f64 = 0.001_931_852_652_41;
const GRAVITY_RATIO_M: f64 = 0.003_449_786_506_84;
#[inline]
fn cross(a: Vec3, b: Vec3) -> Vec3 {
[
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
]
}
pub fn normal_gravity(lat_rad: f64, alt_m: f64) -> f64 {
let sin2 = lat_rad.sin().powi(2);
let e2 = wgs84_e2();
let g0 = GAMMA_E * (1.0 + SOMIGLIANA_K * sin2) / (1.0 - e2 * sin2).sqrt();
let h = alt_m;
let a = WGS84_A;
let term1 = 1.0 - 2.0 / a * (1.0 + WGS84_F + GRAVITY_RATIO_M - 2.0 * WGS84_F * sin2) * h;
let term2 = 3.0 * h * h / (a * a);
g0 * (term1 + term2)
}
fn radius_meridian(lat_rad: f64) -> f64 {
let e2 = wgs84_e2();
let s2 = lat_rad.sin().powi(2);
WGS84_A * (1.0 - e2) / (1.0 - e2 * s2).powf(1.5)
}
fn radius_transverse(lat_rad: f64) -> f64 {
let e2 = wgs84_e2();
let s2 = lat_rad.sin().powi(2);
WGS84_A / (1.0 - e2 * s2).sqrt()
}
pub fn radii_of_curvature(lat_rad: f64) -> (f64, f64) {
(radius_meridian(lat_rad), radius_transverse(lat_rad))
}
#[derive(Clone, Copy, Debug)]
pub struct NavState {
pub q: Quaternion,
pub v_ned: Vec3,
pub p_llh: Geodetic,
}
impl NavState {
pub fn new(q: Quaternion, v_ned: Vec3, p_llh: Geodetic) -> Self {
Self {
q: q.normalized(),
v_ned,
p_llh,
}
}
pub fn omega_ie_n(&self) -> Vec3 {
let l = self.p_llh.lat_rad;
[OMEGA_IE * l.cos(), 0.0, -OMEGA_IE * l.sin()]
}
pub fn omega_en_n(&self) -> Vec3 {
let l = self.p_llh.lat_rad;
let h = self.p_llh.alt_m;
let rn = radius_meridian(l);
let re = radius_transverse(l);
let (vn, ve) = (self.v_ned[0], self.v_ned[1]);
[ve / (re + h), -vn / (rn + h), -ve * l.tan() / (re + h)]
}
pub fn step(&mut self, gyro_b: Vec3, accel_f_b: Vec3, dt: f64) {
let dtheta = [gyro_b[0] * dt, gyro_b[1] * dt, gyro_b[2] * dt];
let dv = [accel_f_b[0] * dt, accel_f_b[1] * dt, accel_f_b[2] * dt];
self.step_increments(dtheta, dv, dt);
}
pub fn step_increments(&mut self, dtheta_b: Vec3, dv_b: Vec3, dt: f64) {
let omega_ie = self.omega_ie_n();
let omega_en = self.omega_en_n();
let omega_in = [
omega_ie[0] + omega_en[0],
omega_ie[1] + omega_en[1],
omega_ie[2] + omega_en[2],
];
let q_old = self.q;
let zeta = [omega_in[0] * dt, omega_in[1] * dt, omega_in[2] * dt];
let neg_zeta = [-zeta[0], -zeta[1], -zeta[2]];
let q_nav = Quaternion::from_rotation_vector(neg_zeta);
let q_body = Quaternion::from_rotation_vector(dtheta_b);
let q_new = q_nav.mul(&q_old).mul(&q_body).normalized();
let zeta_b = q_old.conjugate().rotate(zeta);
let dtheta_rel = [
dtheta_b[0] - zeta_b[0],
dtheta_b[1] - zeta_b[1],
dtheta_b[2] - zeta_b[2],
];
let dv_rot = sculling_increment(dtheta_rel, dv_b);
let dv_body = [
dv_b[0] + dv_rot[0],
dv_b[1] + dv_rot[1],
dv_b[2] + dv_rot[2],
];
let dv_n = q_old.rotate(dv_body);
let g = normal_gravity(self.p_llh.lat_rad, self.p_llh.alt_m);
let rot = [
2.0 * omega_ie[0] + omega_en[0],
2.0 * omega_ie[1] + omega_en[1],
2.0 * omega_ie[2] + omega_en[2],
];
let cor = cross(rot, self.v_ned);
let v_old = self.v_ned;
let v_new = [
v_old[0] + dv_n[0] - cor[0] * dt,
v_old[1] + dv_n[1] - cor[1] * dt,
v_old[2] + dv_n[2] + (g - cor[2]) * dt,
];
let l = self.p_llh.lat_rad;
let h = self.p_llh.alt_m;
let rn = radius_meridian(l);
let re = radius_transverse(l);
let vn = 0.5 * (v_old[0] + v_new[0]);
let ve = 0.5 * (v_old[1] + v_new[1]);
let vd = 0.5 * (v_old[2] + v_new[2]);
let lat = l + dt * vn / (rn + h);
let lon = self.p_llh.lon_rad + dt * ve / ((re + h) * l.cos());
let alt = h - dt * vd;
self.q = q_new;
self.v_ned = v_new;
self.p_llh = Geodetic {
lat_rad: lat,
lon_rad: lon,
alt_m: alt,
};
}
}
pub fn sculling_increment(dtheta_b: Vec3, dv_b: Vec3) -> Vec3 {
let c = cross(dtheta_b, dv_b);
[0.5 * c[0], 0.5 * c[1], 0.5 * c[2]]
}
#[cfg(test)]
mod tests {
use super::*;
use crate::frames::geodetic_to_ecef;
use std::f64::consts::PI;
fn ecef_distance(a: Geodetic, b: Geodetic) -> f64 {
let pa = geodetic_to_ecef(a);
let pb = geodetic_to_ecef(b);
((pa[0] - pb[0]).powi(2) + (pa[1] - pb[1]).powi(2) + (pa[2] - pb[2]).powi(2)).sqrt()
}
#[test]
fn normal_gravity_matches_known_surface_values() {
assert!((normal_gravity(0.0, 0.0) - 9.780_325).abs() < 1e-4);
assert!((normal_gravity(PI / 2.0, 0.0) - 9.832_185).abs() < 1e-4);
let g45 = normal_gravity(PI / 4.0, 0.0);
assert!((g45 - 9.806_2).abs() < 1e-3, "g(45deg)={g45}");
}
#[test]
fn gravity_decreases_with_altitude() {
let g0 = normal_gravity(PI / 4.0, 0.0);
let g100 = normal_gravity(PI / 4.0, 100_000.0);
assert!(g100 < g0);
assert!((g0 - g100 - 0.307).abs() < 0.02, "drop={}", g0 - g100);
}
#[test]
fn static_platform_on_rotating_earth_stays_put() {
let lat = PI / 4.0;
let start = Geodetic {
lat_rad: lat,
lon_rad: 0.2,
alt_m: 120.0,
};
let mut nav = NavState::new(Quaternion::identity(), [0.0, 0.0, 0.0], start);
let gyro = [OMEGA_IE * lat.cos(), 0.0, -OMEGA_IE * lat.sin()];
let g = normal_gravity(lat, 120.0);
let accel = [0.0, 0.0, -g];
let dt = 0.01;
for _ in 0..6_000 {
nav.step(gyro, accel, dt);
}
let drift = ecef_distance(start, nav.p_llh);
let speed = (nav.v_ned[0].powi(2) + nav.v_ned[1].powi(2) + nav.v_ned[2].powi(2)).sqrt();
assert!(drift < 1e-3, "static drift {drift} m exceeds 1 mm");
assert!(speed < 1e-5, "static speed {speed} m/s");
}
#[test]
fn level_north_specific_force_accelerates_north() {
let lat = 0.0;
let start = Geodetic {
lat_rad: lat,
lon_rad: 0.0,
alt_m: 0.0,
};
let mut nav = NavState::new(Quaternion::identity(), [0.0, 0.0, 0.0], start);
let g = normal_gravity(lat, 0.0);
let a = 0.5;
let accel = [a, 0.0, -g]; let dt = 0.001;
let n = 10_000; for _ in 0..n {
let vn = nav.v_ned[0];
let gyro = [OMEGA_IE, -vn / WGS84_A, 0.0];
nav.step(gyro, accel, dt);
}
let t = n as f64 * dt;
assert!(
(nav.v_ned[0] - a * t).abs() < 1e-2,
"v_N={} expected {}",
nav.v_ned[0],
a * t
);
let rn = radius_meridian(lat);
let north_disp = (nav.p_llh.lat_rad - lat) * (rn + start.alt_m);
let expected = 0.5 * a * t * t;
assert!(
(north_disp - expected).abs() / expected < 1e-3,
"north disp {north_disp} expected {expected}"
);
assert!(nav.v_ned[2].abs() < 1e-3 && nav.v_ned[1].abs() < 1e-3);
}
#[test]
fn radii_of_curvature_are_ordered_and_bounded() {
for &lat_deg in &[0.0, 30.0, 60.0, 89.0] {
let l = lat_deg * PI / 180.0;
let rn = radius_meridian(l);
let re = radius_transverse(l);
assert!(rn <= re + 1.0, "R_N>{re} at {lat_deg}");
}
assert!((radius_transverse(0.0) - WGS84_A).abs() < 1e-6);
}
#[test]
fn sculling_increment_properties() {
assert_eq!(
sculling_increment([0.0, 0.0, 0.02], [0.0, 0.0, 1.5]),
[0.0, 0.0, 0.0]
);
let s = sculling_increment([0.01, 0.0, 0.0], [0.0, 2.0, 0.0]);
assert!((s[2] - 0.5 * 0.01 * 2.0).abs() < 1e-15);
assert!(s[0].abs() < 1e-18 && s[1].abs() < 1e-18);
}
#[test]
fn step_increments_matches_rate_step_in_the_smooth_limit() {
let start = Geodetic {
lat_rad: 0.4,
lon_rad: -1.1,
alt_m: 50.0,
};
let gyro = [1e-3, -2e-3, 5e-4];
let g = normal_gravity(0.4, 50.0);
let accel = [0.2, -0.1, -g];
let dt = 0.01;
let mut a = NavState::new(Quaternion::identity(), [3.0, 1.0, 0.0], start);
let mut b = a;
for _ in 0..500 {
a.step(gyro, accel, dt);
b.step_increments(
[gyro[0] * dt, gyro[1] * dt, gyro[2] * dt],
[accel[0] * dt, accel[1] * dt, accel[2] * dt],
dt,
);
}
assert!((a.v_ned[0] - b.v_ned[0]).abs() < 1e-12);
assert!((a.p_llh.lat_rad - b.p_llh.lat_rad).abs() < 1e-15);
}
}