1use crate::astro::constants::earth::OMEGA_E_DOT_RAD_S;
4use crate::astro::math::mat3::Mat3;
5use crate::astro::math::vec3::{add3, dot3, scale3};
6
7use super::config::MechanizationConfig;
8use super::frames::gravity_ecef_mps2;
9use super::imu::{CorrectedImuIncrement, ImuErrorModel, ImuSample};
10use super::state::{
11 mat3_add, mat3_identity, mat3_mul, mat3_mul_vec, mat3_scale, reorthonormalize_dcm, skew,
12 NavState,
13};
14use super::{validate_finite, validate_vec3, InertialError};
15
16#[derive(Debug, Clone, Copy, PartialEq)]
18pub struct StrapdownMechanizer {
19 state: NavState,
20 imu_model: ImuErrorModel,
21 config: MechanizationConfig,
22}
23
24impl StrapdownMechanizer {
25 pub fn new(state: NavState) -> Result<Self, InertialError> {
27 state.validate()?;
28 Ok(Self {
29 state,
30 imu_model: ImuErrorModel::default(),
31 config: MechanizationConfig::default(),
32 })
33 }
34
35 pub fn with_imu_model(mut self, imu_model: ImuErrorModel) -> Result<Self, InertialError> {
37 imu_model.bias.validate()?;
38 imu_model.calibration.validate()?;
39 self.imu_model = imu_model;
40 Ok(self)
41 }
42
43 pub const fn with_config(mut self, config: MechanizationConfig) -> Self {
45 self.config = config;
46 self
47 }
48
49 pub const fn state(&self) -> &NavState {
51 &self.state
52 }
53
54 pub fn propagate(&mut self, sample: ImuSample) -> Result<&NavState, InertialError> {
56 let increment = self
57 .imu_model
58 .correct_sample(&sample, self.state.t_j2000_s)?;
59 self.state = mechanize_ecef(&self.state, &increment, self.config)?;
60 Ok(&self.state)
61 }
62}
63
64pub fn mechanize_ecef(
66 state: &NavState,
67 increment: &CorrectedImuIncrement,
68 _config: MechanizationConfig,
69) -> Result<NavState, InertialError> {
70 state.validate()?;
71 validate_increment(increment)?;
72 if increment.t_j2000_s <= state.t_j2000_s {
73 return Err(InertialError::NonMonotonicSample);
74 }
75
76 let dt_s = increment.dt_s;
77 let body_delta = rodrigues_delta_dcm(increment.delta_theta_rad)?;
78 let earth_delta = earth_rotation_first_order(dt_s);
79 let attitude_raw = mat3_mul(
80 &mat3_mul(&earth_delta, &state.attitude_body_to_ecef),
81 &body_delta,
82 );
83 let attitude_body_to_ecef = reorthonormalize_dcm(&attitude_raw)?;
84
85 let c_avg = mid_interval_dcm(
86 &state.attitude_body_to_ecef,
87 increment.delta_theta_rad,
88 dt_s,
89 )?;
90 let delta_v_ecef = mat3_mul_vec(&c_avg, increment.delta_velocity_mps);
91 let gravity = gravity_ecef_mps2(state.position_ecef_m)?;
92 let coriolis = scale3(earth_rate_cross(state.velocity_ecef_mps), -2.0);
93 let acceleration = add3(gravity, coriolis);
94 let velocity_ecef_mps = add3(
95 add3(state.velocity_ecef_mps, delta_v_ecef),
96 scale3(acceleration, dt_s),
97 );
98 let avg_velocity = scale3(add3(state.velocity_ecef_mps, velocity_ecef_mps), 0.5);
99 let position_ecef_m = add3(state.position_ecef_m, scale3(avg_velocity, dt_s));
100
101 NavState {
102 t_j2000_s: increment.t_j2000_s,
103 position_ecef_m,
104 velocity_ecef_mps,
105 attitude_body_to_ecef,
106 accel_bias_mps2: state.accel_bias_mps2,
107 gyro_bias_rps: state.gyro_bias_rps,
108 }
109 .with_biases(state.accel_bias_mps2, state.gyro_bias_rps)
110}
111
112pub fn rodrigues_delta_dcm(delta_theta_rad: [f64; 3]) -> Result<Mat3, InertialError> {
114 validate_vec3(delta_theta_rad, "delta_theta_rad")?;
115 let phi2 = dot3(delta_theta_rad, delta_theta_rad);
116 let phi = phi2.sqrt();
117 let phi4 = phi2 * phi2;
118 let (a, b) = if phi < 1.0e-8 {
119 (
120 1.0 - phi2 / 6.0 + phi4 / 120.0,
121 0.5 - phi2 / 24.0 + phi4 / 720.0,
122 )
123 } else {
124 (phi.sin() / phi, (1.0 - phi.cos()) / phi2)
125 };
126 let k = skew(delta_theta_rad);
127 let k2 = mat3_mul(&k, &k);
128 Ok(mat3_add(
129 &mat3_add(&mat3_identity(), &mat3_scale(&k, a)),
130 &mat3_scale(&k2, b),
131 ))
132}
133
134pub(crate) fn validate_increment(increment: &CorrectedImuIncrement) -> Result<(), InertialError> {
135 validate_finite(increment.t_j2000_s, "increment.t_j2000_s")?;
136 validate_vec3(increment.delta_velocity_mps, "increment.delta_velocity_mps")?;
137 validate_vec3(increment.delta_theta_rad, "increment.delta_theta_rad")?;
138 validate_finite(increment.dt_s, "increment.dt_s")?;
139 if increment.dt_s > 0.0 {
140 Ok(())
141 } else {
142 Err(super::invalid_input("increment.dt_s", "must be positive"))
143 }
144}
145
146pub(crate) fn mid_interval_dcm(
147 attitude_body_to_ecef: &Mat3,
148 delta_theta_rad: [f64; 3],
149 dt_s: f64,
150) -> Result<Mat3, InertialError> {
151 let earth_half = earth_rotation_first_order(0.5 * dt_s);
152 let body_half = rodrigues_delta_dcm(scale3(delta_theta_rad, 0.5))?;
153 reorthonormalize_dcm(&mat3_mul(
154 &mat3_mul(&earth_half, attitude_body_to_ecef),
155 &body_half,
156 ))
157}
158
159pub(crate) fn earth_rotation_first_order(dt_s: f64) -> Mat3 {
160 [
161 [1.0, OMEGA_E_DOT_RAD_S * dt_s, 0.0],
162 [-OMEGA_E_DOT_RAD_S * dt_s, 1.0, 0.0],
163 [0.0, 0.0, 1.0],
164 ]
165}
166
167pub(crate) fn earth_rate_cross(v: [f64; 3]) -> [f64; 3] {
168 [-OMEGA_E_DOT_RAD_S * v[1], OMEGA_E_DOT_RAD_S * v[0], 0.0]
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
180 use crate::astro::constants::earth::{WGS84_A_M, WGS84_F};
181 use crate::astro::math::mat3::inline_tr;
182 use crate::astro::math::vec3::sub3;
183
184 fn assert_close(actual: f64, expected: f64, tolerance: f64) {
185 assert!(
186 (actual - expected).abs() <= tolerance,
187 "actual {actual:.17e}, expected {expected:.17e}, tolerance {tolerance:.17e}"
188 );
189 }
190
191 fn assert_vec_close(actual: [f64; 3], expected: [f64; 3], tolerance: f64) {
192 for i in 0..3 {
193 assert_close(actual[i], expected[i], tolerance);
194 }
195 }
196
197 fn inverse_delta_velocity(
198 state: &NavState,
199 target_velocity_ecef_mps: [f64; 3],
200 delta_theta_rad: [f64; 3],
201 dt_s: f64,
202 ) -> [f64; 3] {
203 let c_avg = mid_interval_dcm(&state.attitude_body_to_ecef, delta_theta_rad, dt_s)
204 .expect("mid-interval dcm");
205 let c_avg_t = inline_tr(&c_avg);
206 let gravity = gravity_ecef_mps2(state.position_ecef_m).expect("gravity");
207 let coriolis = scale3(earth_rate_cross(state.velocity_ecef_mps), -2.0);
208 let acceleration = add3(gravity, coriolis);
209 let required_ecef = sub3(
210 sub3(target_velocity_ecef_mps, state.velocity_ecef_mps),
211 scale3(acceleration, dt_s),
212 );
213 mat3_mul_vec(&c_avg_t, required_ecef)
214 }
215
216 #[test]
217 fn rodrigues_hits_right_angle_rotation() {
218 let dcm = rodrigues_delta_dcm([0.0, 0.0, core::f64::consts::FRAC_PI_2]).expect("rodrigues");
219 assert_close(dcm[0][0], 0.0, 1.2e-16);
220 assert_close(dcm[0][1], -1.0, 0.0);
221 assert_close(dcm[1][0], 1.0, 0.0);
222 assert_close(dcm[1][1], 0.0, 1.2e-16);
223 assert_close(dcm[2][2], 1.0, 0.0);
224 }
225
226 #[test]
227 fn attitude_mechanization_matches_closed_form_update() {
228 let state =
229 NavState::new(0.0, [WGS84_A_M, 0.0, 0.0], [0.0; 3], mat3_identity()).expect("state");
230 let dt_s = 0.25;
231 let delta_theta_rad = [0.0, 0.0, 0.9];
232 let delta_velocity_mps = inverse_delta_velocity(&state, [0.0; 3], delta_theta_rad, dt_s);
233 let increment = CorrectedImuIncrement {
234 t_j2000_s: dt_s,
235 delta_velocity_mps,
236 delta_theta_rad,
237 dt_s,
238 };
239 let propagated =
240 mechanize_ecef(&state, &increment, MechanizationConfig::default()).expect("step");
241
242 let expected = reorthonormalize_dcm(&mat3_mul(
243 &earth_rotation_first_order(dt_s),
244 &rodrigues_delta_dcm(delta_theta_rad).expect("delta dcm"),
245 ))
246 .expect("expected");
247 for (i, row) in expected.iter().enumerate() {
248 for (j, expected_value) in row.iter().enumerate() {
249 assert_close(
250 propagated.attitude_body_to_ecef[i][j],
251 *expected_value,
252 3.0e-16,
253 );
254 }
255 }
256 }
257
258 #[test]
259 fn inverted_static_trajectory_holds_velocity_and_position() {
260 let mut state =
261 NavState::new(0.0, [WGS84_A_M, 0.0, 0.0], [0.0; 3], mat3_identity()).expect("state");
262 let dt_s = 0.01;
263 for step in 1..=200 {
264 let delta_theta_rad = [0.0; 3];
265 let delta_velocity_mps =
266 inverse_delta_velocity(&state, [0.0; 3], delta_theta_rad, dt_s);
267 let increment = CorrectedImuIncrement {
268 t_j2000_s: step as f64 * dt_s,
269 delta_velocity_mps,
270 delta_theta_rad,
271 dt_s,
272 };
273 state =
274 mechanize_ecef(&state, &increment, MechanizationConfig::default()).expect("step");
275 }
276 assert_vec_close(state.velocity_ecef_mps, [0.0; 3], 2.0e-14);
277 assert_vec_close(state.position_ecef_m, [WGS84_A_M, 0.0, 0.0], 2.0e-13);
278 }
279
280 #[test]
281 fn inverted_constant_acceleration_trajectory_hits_position() {
282 let b_m = WGS84_A_M * (1.0 - WGS84_F);
283 let state = NavState::new(
284 100.0,
285 [4_510_000.0, 120_000.0, b_m / 2.0],
286 [12.0, -4.0, 1.5],
287 mat3_identity(),
288 )
289 .expect("state");
290 let dt_s = 0.2;
291 let target_velocity = [12.3, -4.1, 1.55];
292 let delta_theta_rad = [0.01, -0.02, 0.03];
293 let delta_velocity_mps =
294 inverse_delta_velocity(&state, target_velocity, delta_theta_rad, dt_s);
295 let increment = CorrectedImuIncrement {
296 t_j2000_s: 100.2,
297 delta_velocity_mps,
298 delta_theta_rad,
299 dt_s,
300 };
301 let propagated =
302 mechanize_ecef(&state, &increment, MechanizationConfig::default()).expect("step");
303 let expected_position = add3(
304 state.position_ecef_m,
305 scale3(add3(state.velocity_ecef_mps, target_velocity), 0.5 * dt_s),
306 );
307 assert_vec_close(propagated.velocity_ecef_mps, target_velocity, 2.0e-14);
308 assert_vec_close(propagated.position_ecef_m, expected_position, 3.0e-10);
309 }
310}