1use crate::astro::math::mat3::{self, Mat3};
14use crate::astro::math::vec3;
15use crate::validate;
16use nalgebra::SMatrix;
17
18const ZERO_POSITION_EPS: f64 = 1.0e-30;
21const PARALLEL_RV_EPS: f64 = 1.0e-30;
24const PSD_DIAGONAL_EPS: f64 = 1.0e-15;
27const PSD_MINOR_EPS: f64 = 1.0e-12;
30const SYMMETRY_EPS: f64 = 1.0e-12;
32const SYMMETRY_REL_EPS6: f64 = 1.0e-12;
34const PSD6_EIGEN_REL_EPS: f64 = 1.0e-10;
36
37pub type Mat6 = [[f64; 6]; 6];
39
40#[derive(Debug, Clone, Copy, PartialEq)]
42pub struct Covariance6 {
43 matrix: Mat6,
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum Covariance6Error {
49 NonFinite,
51 Asymmetric,
53 NotPositiveSemidefinite,
55}
56
57impl Covariance6 {
58 pub fn try_from_matrix(matrix: Mat6) -> Result<Self, Covariance6Error> {
60 if !finite6(&matrix) {
61 return Err(Covariance6Error::NonFinite);
62 }
63 if !symmetric6(&matrix) {
64 return Err(Covariance6Error::Asymmetric);
65 }
66 if !positive_semidefinite6(&matrix) {
67 return Err(Covariance6Error::NotPositiveSemidefinite);
68 }
69 Ok(Self { matrix })
70 }
71
72 pub fn from_diagonal(diagonal: [f64; 6]) -> Result<Self, Covariance6Error> {
74 let mut matrix = [[0.0_f64; 6]; 6];
75 for (idx, value) in diagonal.into_iter().enumerate() {
76 matrix[idx][idx] = value;
77 }
78 Self::try_from_matrix(matrix)
79 }
80
81 pub const fn from_matrix_unchecked(matrix: Mat6) -> Self {
86 Self { matrix }
87 }
88
89 pub const fn as_matrix(&self) -> &Mat6 {
91 &self.matrix
92 }
93
94 pub const fn into_matrix(self) -> Mat6 {
96 self.matrix
97 }
98
99 pub fn position_covariance_km2(&self) -> Mat3 {
101 [
102 [self.matrix[0][0], self.matrix[0][1], self.matrix[0][2]],
103 [self.matrix[1][0], self.matrix[1][1], self.matrix[1][2]],
104 [self.matrix[2][0], self.matrix[2][1], self.matrix[2][2]],
105 ]
106 }
107
108 pub fn is_symmetric(&self) -> bool {
110 symmetric6(&self.matrix)
111 }
112
113 pub fn is_positive_semidefinite(&self) -> bool {
115 positive_semidefinite6(&self.matrix)
116 }
117
118 #[allow(clippy::needless_range_loop)]
121 pub fn propagate_with_stm(&self, stm: &Mat6) -> Result<Self, Covariance6Error> {
122 if !finite6(stm) {
123 return Err(Covariance6Error::NonFinite);
124 }
125
126 let mut temp = [[0.0_f64; 6]; 6];
127 for i in 0..6 {
128 for j in 0..6 {
129 for k in 0..6 {
130 temp[i][j] += stm[i][k] * self.matrix[k][j];
131 }
132 }
133 }
134
135 let mut propagated = [[0.0_f64; 6]; 6];
136 for i in 0..6 {
137 for j in 0..6 {
138 for k in 0..6 {
139 propagated[i][j] += temp[i][k] * stm[j][k];
140 }
141 }
142 }
143 symmetrize6(&mut propagated);
144
145 Self::try_from_matrix(propagated)
146 }
147}
148
149#[derive(Debug, Clone, Copy, PartialEq, Eq)]
151pub enum RtnFrameError {
152 InvalidInput {
154 field: &'static str,
155 reason: &'static str,
156 },
157 ZeroPosition,
159 ParallelPositionVelocity,
161}
162
163impl RtnFrameError {
164 pub fn message(self) -> &'static str {
167 match self {
168 RtnFrameError::InvalidInput { .. } => "invalid input",
169 RtnFrameError::ZeroPosition => "zero position vector",
170 RtnFrameError::ParallelPositionVelocity => "position and velocity are parallel",
171 }
172 }
173}
174
175fn invalid_input(field: &'static str, reason: &'static str) -> RtnFrameError {
176 RtnFrameError::InvalidInput { field, reason }
177}
178
179fn validate_vec3(field: &'static str, values: [f64; 3]) -> Result<(), RtnFrameError> {
180 if values.iter().all(|value| value.is_finite()) {
181 Ok(())
182 } else {
183 Err(invalid_input(field, "components must be finite"))
184 }
185}
186
187fn validate_covariance(field: &'static str, values: &Mat3) -> Result<(), RtnFrameError> {
188 validate::validate_covariance_psd(values, field).map_err(|error| match error {
189 validate::FieldError::NonFinite { field } => {
190 invalid_input(field, "components must be finite")
191 }
192 validate::FieldError::NotPositive { field } => invalid_input(field, "not positive"),
193 validate::FieldError::Negative { field } => invalid_input(field, "negative"),
194 validate::FieldError::OutOfRange { field, .. } => invalid_input(field, "out of range"),
195 validate::FieldError::Missing { field }
196 | validate::FieldError::FloatParse { field, .. }
197 | validate::FieldError::IntParse { field, .. }
198 | validate::FieldError::InvalidCivilDate { field, .. }
199 | validate::FieldError::InvalidCivilTime { field, .. } => invalid_input(field, "invalid"),
200 })
201}
202
203fn validate_mat3_finite(field: &'static str, values: &Mat3) -> Result<(), RtnFrameError> {
204 for row in values {
205 validate_vec3(field, *row)?;
206 }
207 Ok(())
208}
209
210fn rtn_to_eci_rotation(r: [f64; 3], v: [f64; 3]) -> Result<Mat3, RtnFrameError> {
217 validate_vec3("position", r)?;
218 validate_vec3("velocity", v)?;
219 if vec3::norm3(r) < ZERO_POSITION_EPS {
220 return Err(RtnFrameError::ZeroPosition);
221 }
222 let r_hat = vec3::unit3_ref_unchecked(&r);
223 let h = vec3::cross3(r, v);
224 if vec3::norm3(h) < PARALLEL_RV_EPS {
225 return Err(RtnFrameError::ParallelPositionVelocity);
226 }
227 let n_hat = vec3::unit3_ref_unchecked(&h);
228 let t_hat = vec3::cross3(n_hat, r_hat);
229 Ok([
230 [r_hat[0], t_hat[0], n_hat[0]],
231 [r_hat[1], t_hat[1], n_hat[1]],
232 [r_hat[2], t_hat[2], n_hat[2]],
233 ])
234}
235
236pub fn rtn_to_eci(cov_rtn: &Mat3, r: [f64; 3], v: [f64; 3]) -> Result<Mat3, RtnFrameError> {
242 validate_covariance("cov_rtn", cov_rtn)?;
243 let rot = rtn_to_eci_rotation(r, v)?;
244 let rot_t = mat3::inline_tr(&rot);
245 let cov_eci = mat3::inline_rxr(&mat3::inline_rxr(&rot, cov_rtn), &rot_t);
246 validate_mat3_finite("cov_eci", &cov_eci)?;
247 Ok(cov_eci)
248}
249
250pub fn symmetric(m: &Mat3) -> bool {
252 (m[0][1] - m[1][0]).abs() < SYMMETRY_EPS
253 && (m[0][2] - m[2][0]).abs() < SYMMETRY_EPS
254 && (m[1][2] - m[2][1]).abs() < SYMMETRY_EPS
255}
256
257fn det3x3(m: &Mat3) -> f64 {
260 let (a, b, c) = (m[0][0], m[0][1], m[0][2]);
261 let (d, e, f) = (m[1][0], m[1][1], m[1][2]);
262 let (g, h, i) = (m[2][0], m[2][1], m[2][2]);
263 a * (e * i - f * h) - b * (d * i - f * g) + c * (d * h - e * g)
264}
265
266pub fn positive_semidefinite(m: &Mat3) -> bool {
270 if !symmetric(m) {
271 return false;
272 }
273
274 let m11 = m[0][0];
275 let m22 = m[1][1];
276 let m33 = m[2][2];
277 let m12 = m[0][1];
278 let m13 = m[0][2];
279 let m23 = m[1][2];
280
281 let det12 = m11 * m22 - m12 * m12;
282 let det13 = m11 * m33 - m13 * m13;
283 let det23 = m22 * m33 - m23 * m23;
284 let det123 = det3x3(m);
285
286 m11 >= -PSD_DIAGONAL_EPS
287 && m22 >= -PSD_DIAGONAL_EPS
288 && m33 >= -PSD_DIAGONAL_EPS
289 && det12 >= -PSD_MINOR_EPS
290 && det13 >= -PSD_MINOR_EPS
291 && det23 >= -PSD_MINOR_EPS
292 && det123 >= -PSD_MINOR_EPS
293}
294
295fn finite6(m: &Mat6) -> bool {
296 m.iter().flatten().all(|value| value.is_finite())
297}
298
299fn covariance_scale6(m: &Mat6) -> f64 {
300 (0..6).fold(0.0_f64, |scale, idx| scale.max(m[idx][idx].abs()))
301}
302
303#[allow(clippy::needless_range_loop)]
304fn symmetric6(m: &Mat6) -> bool {
305 let tolerance = SYMMETRY_REL_EPS6 * covariance_scale6(m);
306 for i in 0..6 {
307 for j in (i + 1)..6 {
308 if (m[i][j] - m[j][i]).abs() > tolerance {
309 return false;
310 }
311 }
312 }
313 true
314}
315
316fn positive_semidefinite6(m: &Mat6) -> bool {
317 if !finite6(m) || !symmetric6(m) {
318 return false;
319 }
320
321 let matrix = SMatrix::<f64, 6, 6>::from_fn(|i, j| m[i][j]);
322 let eigenvalues = matrix.symmetric_eigen().eigenvalues;
323 let scale = covariance_scale6(m);
324 let floor = -PSD6_EIGEN_REL_EPS * scale;
325 eigenvalues.iter().all(|&lambda| lambda >= floor)
326}
327
328#[allow(clippy::needless_range_loop)]
329fn symmetrize6(m: &mut Mat6) {
330 for i in 0..6 {
331 for j in (i + 1)..6 {
332 let value = 0.5 * (m[i][j] + m[j][i]);
333 m[i][j] = value;
334 m[j][i] = value;
335 }
336 }
337}
338
339#[cfg(test)]
340mod tests {
341 use super::*;
342
343 const RTN_TO_ECI_GOLDEN_BITS: [u64; 9] = [
349 0x4010077f74cce7ac,
350 0xbfd92b0043adb450,
351 0x3fe26dc422b0767a,
352 0xbfd92b0043adb44a,
353 0x402207fb1ad4c218,
354 0xbfb9ef5fd1874930,
355 0x3fe26dc422b0767a,
356 0xbfb9ef5fd1874930,
357 0x402ff4452ac4ca0f,
358 ];
359
360 #[test]
361 fn rtn_to_eci_matches_frozen_elixir_bits() {
362 let r = [7000.123, 1234.5, -250.7];
363 let v = [1.2, 7.4, 0.3];
364 let cov_rtn = [[4.0, 0.5, 0.1], [0.5, 9.0, 0.2], [0.1, 0.2, 16.0]];
365
366 let eci = rtn_to_eci(&cov_rtn, r, v).expect("non-degenerate state");
367
368 let mut flat = [0u64; 9];
369 for (idx, slot) in flat.iter_mut().enumerate() {
370 *slot = eci[idx / 3][idx % 3].to_bits();
371 }
372 assert_eq!(flat, RTN_TO_ECI_GOLDEN_BITS);
373 }
374
375 #[test]
376 fn rtn_to_eci_aligned_state_is_exactly_the_rtn_diagonal() {
377 let r = [7000.0, 0.0, 0.0];
380 let v = [0.0, 7.5, 0.0];
381 let cov_rtn = [[1.0, 0.0, 0.0], [0.0, 2.0, 0.0], [0.0, 0.0, 3.0]];
382
383 let eci = rtn_to_eci(&cov_rtn, r, v).expect("non-degenerate state");
384
385 assert_eq!(eci[0][0].to_bits(), 1.0_f64.to_bits());
386 assert_eq!(eci[1][1].to_bits(), 2.0_f64.to_bits());
387 assert_eq!(eci[2][2].to_bits(), 3.0_f64.to_bits());
388 }
389
390 #[test]
391 fn rtn_to_eci_rejects_zero_position() {
392 let err = rtn_to_eci(&identity(), [0.0, 0.0, 0.0], [0.0, 7.5, 0.0]).unwrap_err();
393 assert_eq!(err, RtnFrameError::ZeroPosition);
394 assert_eq!(err.message(), "zero position vector");
395 }
396
397 #[test]
398 fn rtn_to_eci_rejects_parallel_position_velocity() {
399 let err = rtn_to_eci(&identity(), [7000.0, 0.0, 0.0], [1.0, 0.0, 0.0]).unwrap_err();
400 assert_eq!(err, RtnFrameError::ParallelPositionVelocity);
401 assert_eq!(err.message(), "position and velocity are parallel");
402 }
403
404 #[test]
405 fn rtn_to_eci_rejects_nonfinite_geometry_and_covariance() {
406 let err = rtn_to_eci(&identity(), [7000.0, f64::NAN, 0.0], [0.0, 7.5, 0.0]).unwrap_err();
407 assert_eq!(
408 err,
409 RtnFrameError::InvalidInput {
410 field: "position",
411 reason: "components must be finite",
412 }
413 );
414
415 let err =
416 rtn_to_eci(&identity(), [7000.0, 0.0, 0.0], [0.0, f64::INFINITY, 0.0]).unwrap_err();
417 assert_eq!(
418 err,
419 RtnFrameError::InvalidInput {
420 field: "velocity",
421 reason: "components must be finite",
422 }
423 );
424
425 let mut cov = identity();
426 cov[2][1] = f64::NEG_INFINITY;
427 let err = rtn_to_eci(&cov, [7000.0, 0.0, 0.0], [0.0, 7.5, 0.0]).unwrap_err();
428 assert_eq!(
429 err,
430 RtnFrameError::InvalidInput {
431 field: "cov_rtn",
432 reason: "components must be finite",
433 }
434 );
435 }
436
437 #[test]
438 fn rtn_to_eci_rejects_invalid_covariance_geometry() {
439 let r = [7000.0, 0.0, 0.0];
440 let v = [0.0, 7.5, 0.0];
441
442 let mut negative_variance = identity();
443 negative_variance[0][0] = -1.0;
444 let err = rtn_to_eci(&negative_variance, r, v).unwrap_err();
445 assert_eq!(
446 err,
447 RtnFrameError::InvalidInput {
448 field: "cov_rtn",
449 reason: "not positive",
450 }
451 );
452
453 let asymmetric = [[1.0, 0.5, 0.0], [0.4, 1.0, 0.0], [0.0, 0.0, 1.0]];
454 let err = rtn_to_eci(&asymmetric, r, v).unwrap_err();
455 assert_eq!(
456 err,
457 RtnFrameError::InvalidInput {
458 field: "cov_rtn",
459 reason: "not positive",
460 }
461 );
462
463 let indefinite = [[1.0, 2.0, 0.0], [2.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
464 let err = rtn_to_eci(&indefinite, r, v).unwrap_err();
465 assert_eq!(
466 err,
467 RtnFrameError::InvalidInput {
468 field: "cov_rtn",
469 reason: "not positive",
470 }
471 );
472 }
473
474 #[test]
475 fn positive_semidefinite_accepts_identity_rejects_negative_and_asymmetric() {
476 assert!(positive_semidefinite(&identity()));
477
478 let negative_diag = [[-1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
479 assert!(!positive_semidefinite(&negative_diag));
480
481 let asymmetric = [[1.0, 0.5, 0.0], [0.4, 1.0, 0.0], [0.0, 0.0, 1.0]];
482 assert!(!symmetric(&asymmetric));
483 assert!(!positive_semidefinite(&asymmetric));
484 }
485
486 #[test]
487 fn positive_semidefinite_rejects_symmetric_indefinite_matrix() {
488 let indefinite = [[1.0, 2.0, 0.0], [2.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
490 assert!(symmetric(&indefinite));
491 assert!(!positive_semidefinite(&indefinite));
492 }
493
494 #[test]
495 fn covariance6_accepts_diagonal_and_rejects_bad_matrices() {
496 let covariance =
497 Covariance6::from_diagonal([1.0, 2.0, 3.0, 1.0e-6, 2.0e-6, 3.0e-6]).unwrap();
498 assert!(covariance.is_symmetric());
499 assert!(covariance.is_positive_semidefinite());
500
501 let mut asymmetric = *covariance.as_matrix();
502 asymmetric[0][1] = 1.0e-3;
503 assert_eq!(
504 Covariance6::try_from_matrix(asymmetric),
505 Err(Covariance6Error::Asymmetric)
506 );
507
508 let mut indefinite = *covariance.as_matrix();
509 indefinite[5][5] = -1.0;
510 assert_eq!(
511 Covariance6::try_from_matrix(indefinite),
512 Err(Covariance6Error::NotPositiveSemidefinite)
513 );
514 }
515
516 #[test]
517 fn covariance6_scales_psd_tolerance_to_covariance_magnitude() {
518 let mut large = [[0.0_f64; 6]; 6];
519 for (idx, row) in large.iter_mut().enumerate() {
520 row[idx] = 1.0e18;
521 }
522 large[0][1] = 2.5e17;
523 large[1][0] = 2.5e17 + 1.0e3;
524
525 let covariance = Covariance6::try_from_matrix(large).expect("large PSD covariance");
526 assert!(covariance.is_symmetric());
527 assert!(covariance.is_positive_semidefinite());
528
529 let mut indefinite = large;
530 indefinite[2][2] = -1.0e9;
531 assert_eq!(
532 Covariance6::try_from_matrix(indefinite),
533 Err(Covariance6Error::NotPositiveSemidefinite)
534 );
535
536 let small =
537 Covariance6::from_diagonal([1.0e-18, 2.0e-18, 3.0e-18, 4.0e-18, 5.0e-18, 6.0e-18])
538 .expect("small PSD covariance");
539 assert!(small.is_symmetric());
540 assert!(small.is_positive_semidefinite());
541
542 let mut small_indefinite = *small.as_matrix();
543 small_indefinite[0][0] = -1.0e-20;
544 assert_eq!(
545 Covariance6::try_from_matrix(small_indefinite),
546 Err(Covariance6Error::NotPositiveSemidefinite)
547 );
548 }
549
550 fn identity() -> Mat3 {
551 [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]
552 }
553}