sidereon_core/inertial/
frames.rs1use crate::astro::constants::earth::{
4 GM_EARTH_M3_S2, OMEGA_E_DOT_RAD_S, WGS84_A_M, WGS84_E2, WGS84_F,
5};
6use crate::astro::math::vec3::{norm3, scale3};
7use crate::frame::{itrf_to_geodetic, ItrfPositionM};
8
9use super::{invalid_input, validate_finite, validate_vec3, InertialError};
10
11pub const WGS84_NORMAL_GRAVITY_EQUATOR_MPS2: f64 = 9.780_325_335_9;
13
14pub const WGS84_NORMAL_GRAVITY_POLE_MPS2: f64 = 9.832_184_937_8;
16
17pub const WGS84_SOMIGLIANA_K: f64 = ((WGS84_A_M * (1.0 - WGS84_F))
19 * WGS84_NORMAL_GRAVITY_POLE_MPS2)
20 / (WGS84_A_M * WGS84_NORMAL_GRAVITY_EQUATOR_MPS2)
21 - 1.0;
22
23pub fn normal_gravity_mps2(lat_rad: f64, height_m: f64) -> Result<f64, InertialError> {
30 validate_finite(lat_rad, "lat_rad")?;
31 validate_finite(height_m, "height_m")?;
32 if !(-core::f64::consts::FRAC_PI_2..=core::f64::consts::FRAC_PI_2).contains(&lat_rad) {
33 return Err(invalid_input("lat_rad", "must be in [-pi/2, pi/2]"));
34 }
35
36 let sin_lat = lat_rad.sin();
37 let sin2 = sin_lat * sin_lat;
38 let surface = WGS84_NORMAL_GRAVITY_EQUATOR_MPS2 * (1.0 + WGS84_SOMIGLIANA_K * sin2)
39 / (1.0 - WGS84_E2 * sin2).sqrt();
40 let b_m = WGS84_A_M * (1.0 - WGS84_F);
41 let m = OMEGA_E_DOT_RAD_S * OMEGA_E_DOT_RAD_S * WGS84_A_M * WGS84_A_M * b_m / GM_EARTH_M3_S2;
42 let height_scale = 1.0
43 - (2.0 / WGS84_A_M) * (1.0 + WGS84_F + m - 2.0 * WGS84_F * sin2) * height_m
44 + 3.0 * height_m * height_m / (WGS84_A_M * WGS84_A_M);
45 let gravity = surface * height_scale;
46 if gravity.is_finite() && gravity > 0.0 {
47 Ok(gravity)
48 } else {
49 Err(invalid_input(
50 "height_m",
51 "outside normal-gravity Taylor domain",
52 ))
53 }
54}
55
56pub fn gravity_ecef_mps2(position_ecef_m: [f64; 3]) -> Result<[f64; 3], InertialError> {
62 validate_vec3(position_ecef_m, "position_ecef_m")?;
63 if norm3(position_ecef_m) <= 0.0 {
64 return Err(invalid_input("position_ecef_m", "must be nonzero"));
65 }
66 let position = ItrfPositionM::new(position_ecef_m[0], position_ecef_m[1], position_ecef_m[2])
67 .map_err(|_| invalid_input("position_ecef_m", "must be finite"))?;
68 let geodetic = itrf_to_geodetic(position)
69 .map_err(|_| invalid_input("position_ecef_m", "must convert to WGS84 geodetic"))?;
70 let gravity = normal_gravity_mps2(geodetic.lat_rad, geodetic.height_m)?;
71 let normal = geodetic_surface_normal_ecef(geodetic.lat_rad, geodetic.lon_rad);
72 Ok(scale3(normal, -gravity))
73}
74
75fn geodetic_surface_normal_ecef(lat_rad: f64, lon_rad: f64) -> [f64; 3] {
76 let (sin_lat, cos_lat) = lat_rad.sin_cos();
77 let (sin_lon, cos_lon) = lon_rad.sin_cos();
78 [cos_lat * cos_lon, cos_lat * sin_lon, sin_lat]
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
88
89 fn assert_close(actual: f64, expected: f64, tolerance: f64) {
90 assert!(
91 (actual - expected).abs() <= tolerance,
92 "actual {actual:.17e}, expected {expected:.17e}, tolerance {tolerance:.17e}"
93 );
94 }
95
96 #[test]
97 fn somigliana_hits_wgs84_equator_and_pole_constants() {
98 let equator = normal_gravity_mps2(0.0, 0.0).expect("equator");
99 assert_eq!(
100 equator.to_bits(),
101 WGS84_NORMAL_GRAVITY_EQUATOR_MPS2.to_bits()
102 );
103
104 let pole = normal_gravity_mps2(core::f64::consts::FRAC_PI_2, 0.0).expect("pole gravity");
105 assert_close(pole, WGS84_NORMAL_GRAVITY_POLE_MPS2, 2.0e-15);
106 }
107
108 #[test]
109 fn gravity_ecef_points_down_at_equator_and_pole() {
110 let equator = gravity_ecef_mps2([WGS84_A_M, 0.0, 0.0]).expect("equator vector");
111 assert_eq!(
112 equator[0].to_bits(),
113 (-WGS84_NORMAL_GRAVITY_EQUATOR_MPS2).to_bits()
114 );
115 assert_eq!(equator[1].to_bits(), (-0.0_f64).to_bits());
116 assert_eq!(equator[2].to_bits(), (-0.0_f64).to_bits());
117
118 let b_m = WGS84_A_M * (1.0 - WGS84_F);
119 let pole = gravity_ecef_mps2([0.0, 0.0, b_m]).expect("pole vector");
120 assert_close(pole[0], 0.0, 1.0e-15);
121 assert_close(pole[1], 0.0, 1.0e-15);
122 assert_close(pole[2], -WGS84_NORMAL_GRAVITY_POLE_MPS2, 2.0e-15);
123 }
124}