1use crate::astro::math::linear::{solve_flat_normal_square_root_into, FlatCholeskySolveScratch};
39use crate::dop::PositionCovariance;
40use crate::estimation::primitives::{nis_gate_threshold, NisGate};
41use crate::validate::{self, FieldError};
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum TrackCoordinateFrame {
49 Ecef,
51 Enu,
53 CallerDefinedCartesian,
55}
56
57#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
59pub enum TrackError {
60 #[error("invalid track input {field}: {reason}")]
62 InvalidInput {
63 field: &'static str,
65 reason: &'static str,
67 },
68 #[error("invalid track dimension {field}: expected {expected}, got {actual}")]
70 DimensionMismatch {
71 field: &'static str,
73 expected: usize,
75 actual: usize,
77 },
78 #[error("track covariance {field} is not positive semidefinite")]
80 NonPositiveSemidefinite {
81 field: &'static str,
83 },
84 #[error("track covariance {field} is not positive definite")]
86 NonPositiveDefinite {
87 field: &'static str,
89 },
90}
91
92#[derive(Debug, Clone, PartialEq)]
98pub struct TrackFilterConfig {
99 pub frame: TrackCoordinateFrame,
101 pub initial_t_s: f64,
103 pub initial_position_m: Vec<f64>,
105 pub initial_velocity_m_s: Vec<f64>,
107 pub initial_covariance: Vec<Vec<f64>>,
109 pub acceleration_variance_spectral_density_m2_s3: f64,
111}
112
113impl TrackFilterConfig {
114 pub fn from_position(
119 frame: TrackCoordinateFrame,
120 initial_t_s: f64,
121 initial_position_m: Vec<f64>,
122 position_covariance_m2: Vec<Vec<f64>>,
123 initial_velocity_variance_m2_s2: f64,
124 acceleration_variance_spectral_density_m2_s3: f64,
125 ) -> Result<Self, TrackError> {
126 let dimension = initial_position_m.len();
127 validate_dimension(dimension, "initial_position_m")?;
128 validate_vector_len(&initial_position_m, dimension, "initial_position_m")?;
129 validate_covariance_matrix(&position_covariance_m2, dimension, "position_covariance_m2")?;
130 validate_nonnegative(
131 initial_velocity_variance_m2_s2,
132 "initial_velocity_variance_m2_s2",
133 )?;
134
135 let mut covariance = vec![vec![0.0; 2 * dimension]; 2 * dimension];
136 for row in 0..dimension {
137 for col in 0..dimension {
138 covariance[row][col] = position_covariance_m2[row][col];
139 }
140 covariance[dimension + row][dimension + row] = initial_velocity_variance_m2_s2;
141 }
142
143 Ok(Self {
144 frame,
145 initial_t_s,
146 initial_position_m,
147 initial_velocity_m_s: vec![0.0; dimension],
148 initial_covariance: covariance,
149 acceleration_variance_spectral_density_m2_s3,
150 })
151 }
152
153 pub fn from_position_velocity(
155 frame: TrackCoordinateFrame,
156 initial_t_s: f64,
157 initial_position_m: Vec<f64>,
158 initial_velocity_m_s: Vec<f64>,
159 initial_covariance: Vec<Vec<f64>>,
160 acceleration_variance_spectral_density_m2_s3: f64,
161 ) -> Result<Self, TrackError> {
162 let config = Self {
163 frame,
164 initial_t_s,
165 initial_position_m,
166 initial_velocity_m_s,
167 initial_covariance,
168 acceleration_variance_spectral_density_m2_s3,
169 };
170 config.validate()?;
171 Ok(config)
172 }
173
174 pub fn dimension(&self) -> usize {
176 self.initial_position_m.len()
177 }
178
179 pub fn validate(&self) -> Result<(), TrackError> {
181 validate_time(self.initial_t_s, "initial_t_s")?;
182 validate_dimension(self.dimension(), "initial_position_m")?;
183 validate_vector_len(
184 &self.initial_position_m,
185 self.dimension(),
186 "initial_position_m",
187 )?;
188 validate_vector_len(
189 &self.initial_velocity_m_s,
190 self.dimension(),
191 "initial_velocity_m_s",
192 )?;
193 validate_covariance_matrix(
194 &self.initial_covariance,
195 2 * self.dimension(),
196 "initial_covariance",
197 )?;
198 validate_nonnegative(
199 self.acceleration_variance_spectral_density_m2_s3,
200 "acceleration_variance_spectral_density_m2_s3",
201 )
202 }
203}
204
205#[derive(Debug, Clone, PartialEq)]
207pub struct TrackState {
208 pub frame: TrackCoordinateFrame,
210 pub t_s: f64,
212 pub position_m: Vec<f64>,
214 pub velocity_m_s: Vec<f64>,
216 pub covariance: Vec<Vec<f64>>,
218}
219
220impl TrackState {
221 pub fn new(
223 frame: TrackCoordinateFrame,
224 t_s: f64,
225 position_m: Vec<f64>,
226 velocity_m_s: Vec<f64>,
227 covariance: Vec<Vec<f64>>,
228 ) -> Result<Self, TrackError> {
229 let state = Self {
230 frame,
231 t_s,
232 position_m,
233 velocity_m_s,
234 covariance,
235 };
236 state.validate()?;
237 Ok(state)
238 }
239
240 pub fn dimension(&self) -> usize {
242 self.position_m.len()
243 }
244
245 pub fn state_dimension(&self) -> usize {
247 2 * self.dimension()
248 }
249
250 pub fn state_vector(&self) -> Vec<f64> {
252 state_vector(&self.position_m, &self.velocity_m_s)
253 }
254
255 pub fn position_covariance_m2(&self) -> Vec<Vec<f64>> {
257 let dimension = self.dimension();
258 self.covariance
259 .iter()
260 .take(dimension)
261 .map(|row| row[..dimension].to_vec())
262 .collect()
263 }
264
265 pub fn position_covariance3_m2(&self) -> Result<[[f64; 3]; 3], TrackError> {
267 if self.dimension() != 3 {
268 return Err(TrackError::DimensionMismatch {
269 field: "position_covariance3_m2",
270 expected: 3,
271 actual: self.dimension(),
272 });
273 }
274 Ok(matrix3_from_rows(&self.position_covariance_m2()))
275 }
276
277 pub fn position3_m(&self) -> Result<[f64; 3], TrackError> {
279 if self.dimension() != 3 {
280 return Err(TrackError::DimensionMismatch {
281 field: "position3_m",
282 expected: 3,
283 actual: self.dimension(),
284 });
285 }
286 Ok([self.position_m[0], self.position_m[1], self.position_m[2]])
287 }
288
289 pub fn velocity3_m_s(&self) -> Result<[f64; 3], TrackError> {
291 if self.dimension() != 3 {
292 return Err(TrackError::DimensionMismatch {
293 field: "velocity3_m_s",
294 expected: 3,
295 actual: self.dimension(),
296 });
297 }
298 Ok([
299 self.velocity_m_s[0],
300 self.velocity_m_s[1],
301 self.velocity_m_s[2],
302 ])
303 }
304
305 pub fn validate(&self) -> Result<(), TrackError> {
307 validate_time(self.t_s, "t_s")?;
308 validate_dimension(self.dimension(), "position_m")?;
309 validate_vector_len(&self.position_m, self.dimension(), "position_m")?;
310 validate_vector_len(&self.velocity_m_s, self.dimension(), "velocity_m_s")?;
311 validate_covariance_matrix(&self.covariance, self.state_dimension(), "covariance")
312 }
313}
314
315#[derive(Debug, Clone, PartialEq)]
317pub struct TrackPrediction {
318 pub dt_s: f64,
320 pub transition: Vec<Vec<f64>>,
322 pub process_noise: Vec<Vec<f64>>,
324 pub predicted: TrackState,
326}
327
328#[derive(Debug, Clone, PartialEq)]
330pub struct TrackInnovation {
331 pub innovation: Vec<f64>,
333 pub innovation_covariance: Vec<Vec<f64>>,
335 pub nis: f64,
337}
338
339impl TrackInnovation {
340 pub fn gate(&self, confidence: f64) -> Result<NisGate, TrackError> {
342 let threshold =
343 nis_gate_threshold(self.innovation.len(), confidence).map_err(|err| match err {
344 crate::estimation::PrimitiveError::InvalidInput { field, reason } => {
345 invalid_input(field, reason)
346 }
347 })?;
348 Ok(NisGate {
349 nis: self.nis,
350 threshold,
351 in_gate: self.nis <= threshold,
352 dof: self.innovation.len(),
353 })
354 }
355}
356
357#[derive(Debug, Clone, PartialEq)]
359pub struct TrackUpdate {
360 pub predicted: TrackState,
362 pub updated: TrackState,
364 pub innovation: TrackInnovation,
366 pub kalman_gain: Vec<Vec<f64>>,
368}
369
370#[derive(Debug, Clone, PartialEq)]
372pub struct TrackGatedUpdate {
373 pub gate: NisGate,
375 pub update: Option<TrackUpdate>,
377 pub state: TrackState,
379}
380
381#[derive(Debug, Clone, PartialEq)]
383pub struct TrackFilter {
384 state: TrackState,
385 acceleration_variance_spectral_density_m2_s3: f64,
386}
387
388impl TrackFilter {
389 pub fn new(config: TrackFilterConfig) -> Result<Self, TrackError> {
391 config.validate()?;
392 let state = TrackState::new(
393 config.frame,
394 config.initial_t_s,
395 config.initial_position_m,
396 config.initial_velocity_m_s,
397 config.initial_covariance,
398 )?;
399 Ok(Self {
400 state,
401 acceleration_variance_spectral_density_m2_s3: config
402 .acceleration_variance_spectral_density_m2_s3,
403 })
404 }
405
406 pub fn from_position(
408 frame: TrackCoordinateFrame,
409 initial_t_s: f64,
410 initial_position_m: Vec<f64>,
411 position_covariance_m2: Vec<Vec<f64>>,
412 initial_velocity_variance_m2_s2: f64,
413 acceleration_variance_spectral_density_m2_s3: f64,
414 ) -> Result<Self, TrackError> {
415 Self::new(TrackFilterConfig::from_position(
416 frame,
417 initial_t_s,
418 initial_position_m,
419 position_covariance_m2,
420 initial_velocity_variance_m2_s2,
421 acceleration_variance_spectral_density_m2_s3,
422 )?)
423 }
424
425 pub fn from_position3(
427 frame: TrackCoordinateFrame,
428 initial_t_s: f64,
429 initial_position_m: [f64; 3],
430 position_covariance_m2: [[f64; 3]; 3],
431 initial_velocity_variance_m2_s2: f64,
432 acceleration_variance_spectral_density_m2_s3: f64,
433 ) -> Result<Self, TrackError> {
434 Self::from_position(
435 frame,
436 initial_t_s,
437 initial_position_m.to_vec(),
438 matrix3_to_rows(position_covariance_m2),
439 initial_velocity_variance_m2_s2,
440 acceleration_variance_spectral_density_m2_s3,
441 )
442 }
443
444 pub fn from_position_velocity(
446 frame: TrackCoordinateFrame,
447 initial_t_s: f64,
448 initial_position_m: Vec<f64>,
449 initial_velocity_m_s: Vec<f64>,
450 initial_covariance: Vec<Vec<f64>>,
451 acceleration_variance_spectral_density_m2_s3: f64,
452 ) -> Result<Self, TrackError> {
453 Self::new(TrackFilterConfig::from_position_velocity(
454 frame,
455 initial_t_s,
456 initial_position_m,
457 initial_velocity_m_s,
458 initial_covariance,
459 acceleration_variance_spectral_density_m2_s3,
460 )?)
461 }
462
463 pub fn state(&self) -> &TrackState {
465 &self.state
466 }
467
468 pub fn dimension(&self) -> usize {
470 self.state.dimension()
471 }
472
473 pub fn acceleration_variance_spectral_density_m2_s3(&self) -> f64 {
475 self.acceleration_variance_spectral_density_m2_s3
476 }
477
478 pub fn predict(&mut self, dt_s: f64) -> Result<TrackPrediction, TrackError> {
480 validate_positive(dt_s, "dt_s")?;
481 let dimension = self.dimension();
482 let transition = transition_matrix(dimension, dt_s);
483 let process_noise = process_noise_matrix(
484 dimension,
485 dt_s,
486 self.acceleration_variance_spectral_density_m2_s3,
487 );
488 let predicted_vector = matvec(&transition, &self.state.state_vector())?;
489 let transition_t = transpose(&transition)?;
490 let fp = matmul(&transition, &self.state.covariance)?;
491 let mut predicted_covariance = matrix_add(&matmul(&fp, &transition_t)?, &process_noise)?;
492 copy_lower_to_upper(&mut predicted_covariance);
493 validate_covariance_matrix(&predicted_covariance, 2 * dimension, "covariance")?;
494
495 self.state = TrackState::new(
496 self.state.frame,
497 self.state.t_s + dt_s,
498 predicted_vector[..dimension].to_vec(),
499 predicted_vector[dimension..].to_vec(),
500 predicted_covariance,
501 )?;
502
503 Ok(TrackPrediction {
504 dt_s,
505 transition,
506 process_noise,
507 predicted: self.state.clone(),
508 })
509 }
510
511 pub fn predict_recorded(
513 &mut self,
514 dt_s: f64,
515 history: &mut TrackRtsHistoryBuilder,
516 ) -> Result<TrackPrediction, TrackError> {
517 let mut working_filter = self.clone();
518 let mut working_history = history.clone();
519 let prediction = working_filter.predict(dt_s)?;
520 working_history
521 .record_prediction(prediction.predicted.clone(), prediction.transition.clone())?;
522 *self = working_filter;
523 *history = working_history;
524 Ok(prediction)
525 }
526
527 pub fn position_innovation(
529 &self,
530 observation_position_m: &[f64],
531 observation_covariance_m2: &[Vec<f64>],
532 ) -> Result<TrackInnovation, TrackError> {
533 let dimension = self.dimension();
534 validate_vector_len(observation_position_m, dimension, "observation_position_m")?;
535 validate_covariance_matrix(
536 observation_covariance_m2,
537 dimension,
538 "observation_covariance_m2",
539 )?;
540 let innovation = observation_position_m
541 .iter()
542 .zip(&self.state.position_m)
543 .map(|(obs, pred)| obs - pred)
544 .collect::<Vec<_>>();
545 let predicted_position_covariance = self.state.position_covariance_m2();
546 let innovation_covariance =
547 matrix_add(&predicted_position_covariance, observation_covariance_m2)?;
548 validate_spd_matrix(&innovation_covariance, dimension, "innovation_covariance")?;
549 let nis = nis_from_innovation(&innovation, &innovation_covariance)?;
550 Ok(TrackInnovation {
551 innovation,
552 innovation_covariance,
553 nis,
554 })
555 }
556
557 pub fn state_innovation(
559 &self,
560 observation_state: &[f64],
561 observation_covariance: &[Vec<f64>],
562 ) -> Result<TrackInnovation, TrackError> {
563 let state_dimension = self.state.state_dimension();
564 validate_vector_len(observation_state, state_dimension, "observation_state")?;
565 validate_covariance_matrix(
566 observation_covariance,
567 state_dimension,
568 "observation_covariance",
569 )?;
570 let predicted = self.state.state_vector();
571 let innovation = observation_state
572 .iter()
573 .zip(predicted)
574 .map(|(obs, pred)| obs - pred)
575 .collect::<Vec<_>>();
576 let innovation_covariance = matrix_add(&self.state.covariance, observation_covariance)?;
577 validate_spd_matrix(
578 &innovation_covariance,
579 state_dimension,
580 "innovation_covariance",
581 )?;
582 let nis = nis_from_innovation(&innovation, &innovation_covariance)?;
583 Ok(TrackInnovation {
584 innovation,
585 innovation_covariance,
586 nis,
587 })
588 }
589
590 pub fn update_position(
592 &mut self,
593 observation_position_m: &[f64],
594 observation_covariance_m2: &[Vec<f64>],
595 ) -> Result<TrackUpdate, TrackError> {
596 let predicted = self.state.clone();
597 let update = self.position_update_from_predicted(
598 predicted,
599 observation_position_m,
600 observation_covariance_m2,
601 )?;
602 self.state = update.updated.clone();
603 Ok(update)
604 }
605
606 pub fn update_position3(
608 &mut self,
609 observation_position_m: [f64; 3],
610 observation_covariance_m2: [[f64; 3]; 3],
611 ) -> Result<TrackUpdate, TrackError> {
612 self.update_position(
613 &observation_position_m,
614 &matrix3_to_rows(observation_covariance_m2),
615 )
616 }
617
618 pub fn update_position_covariance(
623 &mut self,
624 observation_position_m: [f64; 3],
625 observation_covariance: &PositionCovariance,
626 ) -> Result<TrackUpdate, TrackError> {
627 let covariance = covariance_for_frame(self.state.frame, observation_covariance)?;
628 self.update_position3(observation_position_m, covariance)
629 }
630
631 pub fn update_state(
633 &mut self,
634 observation_state: &[f64],
635 observation_covariance: &[Vec<f64>],
636 ) -> Result<TrackUpdate, TrackError> {
637 let predicted = self.state.clone();
638 let update =
639 self.state_update_from_predicted(predicted, observation_state, observation_covariance)?;
640 self.state = update.updated.clone();
641 Ok(update)
642 }
643
644 pub fn update_position_gated(
649 &mut self,
650 observation_position_m: &[f64],
651 observation_covariance_m2: &[Vec<f64>],
652 confidence: f64,
653 ) -> Result<TrackGatedUpdate, TrackError> {
654 let innovation =
655 self.position_innovation(observation_position_m, observation_covariance_m2)?;
656 let gate = innovation.gate(confidence)?;
657 if gate.in_gate {
658 let update = self.update_position(observation_position_m, observation_covariance_m2)?;
659 Ok(TrackGatedUpdate {
660 gate,
661 state: self.state.clone(),
662 update: Some(update),
663 })
664 } else {
665 Ok(TrackGatedUpdate {
666 gate,
667 state: self.state.clone(),
668 update: None,
669 })
670 }
671 }
672
673 pub fn update_position_recorded(
675 &mut self,
676 observation_position_m: &[f64],
677 observation_covariance_m2: &[Vec<f64>],
678 history: &mut TrackRtsHistoryBuilder,
679 ) -> Result<TrackUpdate, TrackError> {
680 history.validate_update_ready()?;
681 let predicted = self.state.clone();
682 let mut working_filter = self.clone();
683 let mut working_history = history.clone();
684 let update =
685 working_filter.update_position(observation_position_m, observation_covariance_m2)?;
686 working_history.record_update(predicted, working_filter.state.clone())?;
687 *self = working_filter;
688 *history = working_history;
689 Ok(update)
690 }
691
692 pub fn update_position_gated_recorded(
694 &mut self,
695 observation_position_m: &[f64],
696 observation_covariance_m2: &[Vec<f64>],
697 confidence: f64,
698 history: &mut TrackRtsHistoryBuilder,
699 ) -> Result<TrackGatedUpdate, TrackError> {
700 history.validate_update_ready()?;
701 let predicted = self.state.clone();
702 let mut working_filter = self.clone();
703 let mut working_history = history.clone();
704 let gated = working_filter.update_position_gated(
705 observation_position_m,
706 observation_covariance_m2,
707 confidence,
708 )?;
709 working_history.record_update(predicted, working_filter.state.clone())?;
710 *self = working_filter;
711 *history = working_history;
712 Ok(gated)
713 }
714
715 pub fn record_prediction_only(
717 &self,
718 history: &mut TrackRtsHistoryBuilder,
719 ) -> Result<(), TrackError> {
720 history.record_update(self.state.clone(), self.state.clone())
721 }
722
723 fn position_update_from_predicted(
724 &self,
725 predicted: TrackState,
726 observation_position_m: &[f64],
727 observation_covariance_m2: &[Vec<f64>],
728 ) -> Result<TrackUpdate, TrackError> {
729 let dimension = predicted.dimension();
730 let innovation =
731 self.position_innovation(observation_position_m, observation_covariance_m2)?;
732 let cross = predicted
733 .covariance
734 .iter()
735 .map(|row| row[..dimension].to_vec())
736 .collect::<Vec<_>>();
737 let kalman_gain = gain_from_cross(&cross, &innovation.innovation_covariance)?;
738 let update_delta = matvec(&kalman_gain, &innovation.innovation)?;
739 let mut updated_vector = predicted.state_vector();
740 for (value, delta) in updated_vector.iter_mut().zip(update_delta) {
741 *value += delta;
742 }
743
744 let hp = predicted
745 .covariance
746 .iter()
747 .take(dimension)
748 .cloned()
749 .collect::<Vec<_>>();
750 let khp = matmul(&kalman_gain, &hp)?;
751 let mut covariance = matrix_sub(&predicted.covariance, &khp)?;
752 copy_lower_to_upper(&mut covariance);
753 validate_covariance_matrix(&covariance, 2 * dimension, "covariance")?;
754
755 let updated = TrackState::new(
756 predicted.frame,
757 predicted.t_s,
758 updated_vector[..dimension].to_vec(),
759 updated_vector[dimension..].to_vec(),
760 covariance,
761 )?;
762 Ok(TrackUpdate {
763 predicted,
764 updated,
765 innovation,
766 kalman_gain,
767 })
768 }
769
770 fn state_update_from_predicted(
771 &self,
772 predicted: TrackState,
773 observation_state: &[f64],
774 observation_covariance: &[Vec<f64>],
775 ) -> Result<TrackUpdate, TrackError> {
776 let state_dimension = predicted.state_dimension();
777 let innovation = self.state_innovation(observation_state, observation_covariance)?;
778 let kalman_gain =
779 gain_from_cross(&predicted.covariance, &innovation.innovation_covariance)?;
780 let update_delta = matvec(&kalman_gain, &innovation.innovation)?;
781 let mut updated_vector = predicted.state_vector();
782 for (value, delta) in updated_vector.iter_mut().zip(update_delta) {
783 *value += delta;
784 }
785
786 let khp = matmul(&kalman_gain, &predicted.covariance)?;
787 let mut covariance = matrix_sub(&predicted.covariance, &khp)?;
788 copy_lower_to_upper(&mut covariance);
789 validate_covariance_matrix(&covariance, state_dimension, "covariance")?;
790
791 let dimension = predicted.dimension();
792 let updated = TrackState::new(
793 predicted.frame,
794 predicted.t_s,
795 updated_vector[..dimension].to_vec(),
796 updated_vector[dimension..].to_vec(),
797 covariance,
798 )?;
799 Ok(TrackUpdate {
800 predicted,
801 updated,
802 innovation,
803 kalman_gain,
804 })
805 }
806}
807
808#[derive(Debug, Clone, PartialEq)]
810pub struct TrackRtsEpoch {
811 pub t_s: f64,
813 pub predicted: TrackState,
815 pub updated: TrackState,
817 pub transition_from_previous: Option<Vec<Vec<f64>>>,
819}
820
821impl TrackRtsEpoch {
822 pub fn new(
824 predicted: TrackState,
825 updated: TrackState,
826 transition_from_previous: Option<Vec<Vec<f64>>>,
827 ) -> Result<Self, TrackError> {
828 let epoch = Self {
829 t_s: predicted.t_s,
830 predicted,
831 updated,
832 transition_from_previous,
833 };
834 epoch.validate()?;
835 Ok(epoch)
836 }
837
838 pub fn validate(&self) -> Result<(), TrackError> {
840 self.predicted.validate()?;
841 self.updated.validate()?;
842 if self.predicted.frame != self.updated.frame {
843 return Err(invalid_input(
844 "frame",
845 "predicted and updated frames differ",
846 ));
847 }
848 if self.predicted.dimension() != self.updated.dimension() {
849 return Err(TrackError::DimensionMismatch {
850 field: "dimension",
851 expected: self.predicted.dimension(),
852 actual: self.updated.dimension(),
853 });
854 }
855 if self.predicted.t_s.to_bits() != self.t_s.to_bits()
856 || self.updated.t_s.to_bits() != self.t_s.to_bits()
857 {
858 return Err(invalid_input("t_s", "state epochs must match"));
859 }
860 if let Some(transition) = &self.transition_from_previous {
861 validate_square_matrix(transition, self.updated.state_dimension(), "transition")?;
862 }
863 Ok(())
864 }
865}
866
867#[derive(Debug, Clone, PartialEq)]
869pub struct TrackRtsHistory {
870 pub epochs: Vec<TrackRtsEpoch>,
872}
873
874impl TrackRtsHistory {
875 pub fn new(epochs: Vec<TrackRtsEpoch>) -> Result<Self, TrackError> {
877 let history = Self { epochs };
878 history.validate()?;
879 Ok(history)
880 }
881
882 pub fn validate(&self) -> Result<(), TrackError> {
884 if self.epochs.is_empty() {
885 return Err(invalid_input("history", "must not be empty"));
886 }
887 let frame = self.epochs[0].updated.frame;
888 let dimension = self.epochs[0].updated.dimension();
889 for (idx, epoch) in self.epochs.iter().enumerate() {
890 epoch.validate()?;
891 if epoch.updated.frame != frame {
892 return Err(invalid_input("frame", "history frames differ"));
893 }
894 if epoch.updated.dimension() != dimension {
895 return Err(TrackError::DimensionMismatch {
896 field: "dimension",
897 expected: dimension,
898 actual: epoch.updated.dimension(),
899 });
900 }
901 match (idx, &epoch.transition_from_previous) {
902 (0, None) => {}
903 (0, Some(_)) => {
904 return Err(invalid_input(
905 "transition_from_previous",
906 "first epoch must not have a transition",
907 ));
908 }
909 (_, Some(transition)) => {
910 validate_square_matrix(transition, 2 * dimension, "transition")?;
911 if epoch.t_s <= self.epochs[idx - 1].t_s {
912 return Err(invalid_input("history", "epochs must be increasing"));
913 }
914 }
915 (_, None) => {
916 return Err(invalid_input(
917 "transition_from_previous",
918 "missing transition",
919 ));
920 }
921 }
922 }
923 Ok(())
924 }
925}
926
927#[derive(Debug, Clone, PartialEq)]
929pub struct TrackRtsHistoryBuilder {
930 epochs: Vec<TrackRtsEpoch>,
931 pending_transition: Option<Vec<Vec<f64>>>,
932 pending_predicted: Option<TrackState>,
933}
934
935impl TrackRtsHistoryBuilder {
936 pub fn from_filter(filter: &TrackFilter) -> Result<Self, TrackError> {
938 let initial = TrackRtsEpoch::new(filter.state.clone(), filter.state.clone(), None)?;
939 Ok(Self {
940 epochs: vec![initial],
941 pending_transition: None,
942 pending_predicted: None,
943 })
944 }
945
946 pub const fn empty() -> Self {
948 Self {
949 epochs: Vec::new(),
950 pending_transition: None,
951 pending_predicted: None,
952 }
953 }
954
955 pub fn record_prediction(
957 &mut self,
958 predicted: TrackState,
959 transition: Vec<Vec<f64>>,
960 ) -> Result<(), TrackError> {
961 predicted.validate()?;
962 validate_square_matrix(&transition, predicted.state_dimension(), "transition")?;
963 let combined = if let Some(previous) = &self.pending_transition {
964 matmul(&transition, previous)?
965 } else {
966 transition
967 };
968 self.pending_transition = Some(combined);
969 self.pending_predicted = Some(predicted);
970 Ok(())
971 }
972
973 pub fn record_update(
975 &mut self,
976 predicted: TrackState,
977 updated: TrackState,
978 ) -> Result<(), TrackError> {
979 let transition = if self.epochs.is_empty() {
980 None
981 } else {
982 Some(
983 self.pending_transition
984 .clone()
985 .ok_or_else(|| invalid_input("transition", "missing propagated interval"))?,
986 )
987 };
988 if let Some(pending) = &self.pending_predicted {
989 if pending.t_s.to_bits() != predicted.t_s.to_bits() {
990 return Err(invalid_input(
991 "predicted",
992 "does not match pending prediction",
993 ));
994 }
995 if pending.dimension() != predicted.dimension() || pending.frame != predicted.frame {
996 return Err(invalid_input("predicted", "does not match pending state"));
997 }
998 }
999 let epoch = TrackRtsEpoch::new(predicted, updated, transition)?;
1000 let mut epochs = self.epochs.clone();
1001 epochs.push(epoch);
1002 TrackRtsHistory {
1003 epochs: epochs.clone(),
1004 }
1005 .validate()?;
1006 self.epochs = epochs;
1007 self.pending_transition = None;
1008 self.pending_predicted = None;
1009 Ok(())
1010 }
1011
1012 pub fn finish(self) -> Result<TrackRtsHistory, TrackError> {
1014 if self.pending_transition.is_some() {
1015 return Err(invalid_input("transition", "unclosed propagated interval"));
1016 }
1017 TrackRtsHistory::new(self.epochs)
1018 }
1019
1020 pub fn validate_update_ready(&self) -> Result<(), TrackError> {
1022 if self.epochs.is_empty() || self.pending_transition.is_some() {
1023 Ok(())
1024 } else {
1025 Err(invalid_input("transition", "missing propagated interval"))
1026 }
1027 }
1028}
1029
1030#[derive(Debug, Clone, PartialEq)]
1032pub struct SmoothedTrackEpoch {
1033 pub t_s: f64,
1035 pub state: TrackState,
1037 pub rts_gain_to_next: Option<Vec<Vec<f64>>>,
1039}
1040
1041#[derive(Debug, Clone, PartialEq)]
1043pub struct SmoothedTrack {
1044 pub epochs: Vec<SmoothedTrackEpoch>,
1046}
1047
1048pub fn rts_smooth(history: &TrackRtsHistory) -> Result<SmoothedTrack, TrackError> {
1050 history.validate()?;
1051 let len = history.epochs.len();
1052 let state_dimension = history.epochs[0].updated.state_dimension();
1053 let mut output: Vec<Option<SmoothedTrackEpoch>> = vec![None; len];
1054
1055 let final_epoch = &history.epochs[len - 1];
1056 output[len - 1] = Some(SmoothedTrackEpoch {
1057 t_s: final_epoch.t_s,
1058 state: final_epoch.updated.clone(),
1059 rts_gain_to_next: None,
1060 });
1061
1062 for idx in (0..len - 1).rev() {
1063 let current = &history.epochs[idx];
1064 let next = &history.epochs[idx + 1];
1065 let next_smoothed = output[idx + 1]
1066 .as_ref()
1067 .expect("next smoothed epoch is populated");
1068 let transition = next
1069 .transition_from_previous
1070 .as_ref()
1071 .ok_or_else(|| invalid_input("transition_from_previous", "missing transition"))?;
1072 let gain = rts_gain(
1073 ¤t.updated.covariance,
1074 transition,
1075 &next.predicted.covariance,
1076 )?;
1077 let next_delta = vector_sub(
1078 &next_smoothed.state.state_vector(),
1079 &next.predicted.state_vector(),
1080 "state_delta",
1081 )?;
1082 let correction = matvec(&gain, &next_delta)?;
1083 let mut smoothed_vector = current.updated.state_vector();
1084 for (value, delta) in smoothed_vector.iter_mut().zip(correction) {
1085 *value += delta;
1086 }
1087
1088 let mut covariance = smoothed_covariance(
1089 ¤t.updated.covariance,
1090 &gain,
1091 &next.predicted.covariance,
1092 &next_smoothed.state.covariance,
1093 )?;
1094 copy_lower_to_upper(&mut covariance);
1095 validate_covariance_matrix(&covariance, state_dimension, "smoothed_covariance")?;
1096
1097 let dimension = current.updated.dimension();
1098 let state = TrackState::new(
1099 current.updated.frame,
1100 current.t_s,
1101 smoothed_vector[..dimension].to_vec(),
1102 smoothed_vector[dimension..].to_vec(),
1103 covariance,
1104 )?;
1105 output[idx] = Some(SmoothedTrackEpoch {
1106 t_s: current.t_s,
1107 state,
1108 rts_gain_to_next: Some(gain),
1109 });
1110 }
1111
1112 let epochs = output
1113 .into_iter()
1114 .map(|epoch| epoch.expect("all smoothed epochs populated"))
1115 .collect::<Vec<_>>();
1116 Ok(SmoothedTrack { epochs })
1117}
1118
1119pub fn smooth_track_rts(history: &TrackRtsHistory) -> Result<SmoothedTrack, TrackError> {
1121 rts_smooth(history)
1122}
1123
1124fn covariance_for_frame(
1125 frame: TrackCoordinateFrame,
1126 covariance: &PositionCovariance,
1127) -> Result<[[f64; 3]; 3], TrackError> {
1128 match frame {
1129 TrackCoordinateFrame::Ecef => Ok(covariance.ecef_m2),
1130 TrackCoordinateFrame::Enu => Ok(covariance.enu_m2),
1131 TrackCoordinateFrame::CallerDefinedCartesian => Err(invalid_input(
1132 "frame",
1133 "PositionCovariance requires ECEF or ENU",
1134 )),
1135 }
1136}
1137
1138fn rts_gain(
1139 filtered_covariance: &[Vec<f64>],
1140 transition: &[Vec<f64>],
1141 predicted_covariance_next: &[Vec<f64>],
1142) -> Result<Vec<Vec<f64>>, TrackError> {
1143 let dimension = filtered_covariance.len();
1144 validate_covariance_matrix(filtered_covariance, dimension, "filtered_covariance")?;
1145 validate_square_matrix(transition, dimension, "transition")?;
1146 validate_spd_matrix(
1147 predicted_covariance_next,
1148 dimension,
1149 "predicted_covariance_next",
1150 )?;
1151 let transition_t = transpose(transition)?;
1152 let cross = matmul(filtered_covariance, &transition_t)?;
1153 gain_from_cross(&cross, predicted_covariance_next)
1154}
1155
1156fn smoothed_covariance(
1157 filtered_covariance: &[Vec<f64>],
1158 gain: &[Vec<f64>],
1159 predicted_covariance_next: &[Vec<f64>],
1160 smoothed_covariance_next: &[Vec<f64>],
1161) -> Result<Vec<Vec<f64>>, TrackError> {
1162 let dimension = filtered_covariance.len();
1163 validate_covariance_matrix(filtered_covariance, dimension, "filtered_covariance")?;
1164 validate_square_matrix(gain, dimension, "rts_gain")?;
1165 validate_covariance_matrix(
1166 predicted_covariance_next,
1167 dimension,
1168 "predicted_covariance_next",
1169 )?;
1170 validate_covariance_matrix(
1171 smoothed_covariance_next,
1172 dimension,
1173 "smoothed_covariance_next",
1174 )?;
1175 let delta = matrix_sub(smoothed_covariance_next, predicted_covariance_next)?;
1176 let left = matmul(gain, &delta)?;
1177 let gain_t = transpose(gain)?;
1178 let adjustment = matmul(&left, &gain_t)?;
1179 matrix_add(filtered_covariance, &adjustment)
1180}
1181
1182fn gain_from_cross(
1183 cross: &[Vec<f64>],
1184 innovation_covariance: &[Vec<f64>],
1185) -> Result<Vec<Vec<f64>>, TrackError> {
1186 let measurement_dimension = innovation_covariance.len();
1187 validate_spd_matrix(
1188 innovation_covariance,
1189 measurement_dimension,
1190 "innovation_covariance",
1191 )?;
1192 if cross.is_empty() {
1193 return Err(invalid_input("cross_covariance", "must not be empty"));
1194 }
1195 for row in cross {
1196 validate_vector_len(row, measurement_dimension, "cross_covariance")?;
1197 }
1198
1199 if measurement_dimension == 1 {
1200 let variance = innovation_covariance[0][0];
1201 return Ok(cross
1202 .iter()
1203 .map(|row| vec![row[0] / variance])
1204 .collect::<Vec<_>>());
1205 }
1206
1207 let mut scratch = FlatCholeskySolveScratch::default();
1208 let flat = flatten(innovation_covariance)?;
1209 let mut gain = Vec::with_capacity(cross.len());
1210 for row in cross {
1211 let solved = solve_flat_normal_square_root_into(&flat, row, &mut scratch)
1212 .ok_or(TrackError::NonPositiveDefinite {
1213 field: "innovation_covariance",
1214 })?
1215 .to_vec();
1216 gain.push(solved);
1217 }
1218 Ok(gain)
1219}
1220
1221fn nis_from_innovation(
1222 innovation: &[f64],
1223 innovation_covariance: &[Vec<f64>],
1224) -> Result<f64, TrackError> {
1225 if innovation.len() == 1
1226 && innovation_covariance.len() == 1
1227 && innovation_covariance[0].len() == 1
1228 {
1229 let variance = innovation_covariance[0][0];
1230 validate_positive(variance, "innovation_covariance")?;
1231 let nis = innovation[0] * innovation[0] / variance;
1232 validate_time(nis, "nis")?;
1233 return Ok(nis);
1234 }
1235
1236 let mut scratch = FlatCholeskySolveScratch::default();
1237 let flat = flatten(innovation_covariance)?;
1238 let solved = solve_flat_normal_square_root_into(&flat, innovation, &mut scratch).ok_or(
1239 TrackError::NonPositiveDefinite {
1240 field: "innovation_covariance",
1241 },
1242 )?;
1243 let mut nis = 0.0;
1244 for (lhs, rhs) in innovation.iter().zip(solved) {
1245 nis += lhs * rhs;
1246 }
1247 validate_time(nis, "nis")?;
1248 Ok(nis)
1249}
1250
1251fn transition_matrix(dimension: usize, dt_s: f64) -> Vec<Vec<f64>> {
1252 let state_dimension = 2 * dimension;
1253 let mut transition = vec![vec![0.0; state_dimension]; state_dimension];
1254 for axis in 0..dimension {
1255 transition[axis][axis] = 1.0;
1256 transition[axis][dimension + axis] = dt_s;
1257 transition[dimension + axis][dimension + axis] = 1.0;
1258 }
1259 transition
1260}
1261
1262fn process_noise_matrix(
1263 dimension: usize,
1264 dt_s: f64,
1265 acceleration_variance_spectral_density_m2_s3: f64,
1266) -> Vec<Vec<f64>> {
1267 let state_dimension = 2 * dimension;
1268 let mut process_noise = vec![vec![0.0; state_dimension]; state_dimension];
1269 let q00 = acceleration_variance_spectral_density_m2_s3 * dt_s * dt_s * dt_s / 3.0;
1270 let q01 = acceleration_variance_spectral_density_m2_s3 * dt_s * dt_s / 2.0;
1271 let q11 = acceleration_variance_spectral_density_m2_s3 * dt_s;
1272 for axis in 0..dimension {
1273 process_noise[axis][axis] = q00;
1274 process_noise[axis][dimension + axis] = q01;
1275 process_noise[dimension + axis][axis] = q01;
1276 process_noise[dimension + axis][dimension + axis] = q11;
1277 }
1278 process_noise
1279}
1280
1281fn state_vector(position_m: &[f64], velocity_m_s: &[f64]) -> Vec<f64> {
1282 let mut state = Vec::with_capacity(position_m.len() + velocity_m_s.len());
1283 state.extend(position_m);
1284 state.extend(velocity_m_s);
1285 state
1286}
1287
1288fn matrix3_to_rows(matrix: [[f64; 3]; 3]) -> Vec<Vec<f64>> {
1289 matrix.iter().map(|row| row.to_vec()).collect()
1290}
1291
1292fn matrix3_from_rows(rows: &[Vec<f64>]) -> [[f64; 3]; 3] {
1293 [
1294 [rows[0][0], rows[0][1], rows[0][2]],
1295 [rows[1][0], rows[1][1], rows[1][2]],
1296 [rows[2][0], rows[2][1], rows[2][2]],
1297 ]
1298}
1299
1300fn validate_time(value: f64, field: &'static str) -> Result<(), TrackError> {
1301 if value.is_finite() {
1302 Ok(())
1303 } else {
1304 Err(invalid_input(field, "not finite"))
1305 }
1306}
1307
1308fn validate_positive(value: f64, field: &'static str) -> Result<(), TrackError> {
1309 validate_time(value, field)?;
1310 if value > 0.0 {
1311 Ok(())
1312 } else {
1313 Err(invalid_input(field, "must be positive"))
1314 }
1315}
1316
1317fn validate_nonnegative(value: f64, field: &'static str) -> Result<(), TrackError> {
1318 validate_time(value, field)?;
1319 if value >= 0.0 {
1320 Ok(())
1321 } else {
1322 Err(invalid_input(field, "must be non-negative"))
1323 }
1324}
1325
1326fn validate_dimension(dimension: usize, field: &'static str) -> Result<(), TrackError> {
1327 if dimension > 0 {
1328 Ok(())
1329 } else {
1330 Err(invalid_input(field, "dimension must be positive"))
1331 }
1332}
1333
1334fn validate_vector_len(
1335 values: &[f64],
1336 expected: usize,
1337 field: &'static str,
1338) -> Result<(), TrackError> {
1339 if values.len() != expected {
1340 return Err(TrackError::DimensionMismatch {
1341 field,
1342 expected,
1343 actual: values.len(),
1344 });
1345 }
1346 validate::finite_slice(values, field).map_err(map_field_error)
1347}
1348
1349fn validate_square_matrix(
1350 matrix: &[Vec<f64>],
1351 dimension: usize,
1352 field: &'static str,
1353) -> Result<(), TrackError> {
1354 if matrix.len() != dimension {
1355 return Err(TrackError::DimensionMismatch {
1356 field,
1357 expected: dimension,
1358 actual: matrix.len(),
1359 });
1360 }
1361 for row in matrix {
1362 if row.len() != dimension {
1363 return Err(TrackError::DimensionMismatch {
1364 field,
1365 expected: dimension,
1366 actual: row.len(),
1367 });
1368 }
1369 validate::finite_slice(row, field).map_err(map_field_error)?;
1370 }
1371 Ok(())
1372}
1373
1374fn validate_covariance_matrix(
1375 matrix: &[Vec<f64>],
1376 dimension: usize,
1377 field: &'static str,
1378) -> Result<(), TrackError> {
1379 validate_square_matrix(matrix, dimension, field)?;
1380 let rows = matrix.iter().map(Vec::as_slice).collect::<Vec<_>>();
1381 validate::validate_covariance_psd_rows(&rows, field)
1382 .map_err(|_| TrackError::NonPositiveSemidefinite { field })
1383}
1384
1385fn validate_spd_matrix(
1386 matrix: &[Vec<f64>],
1387 dimension: usize,
1388 field: &'static str,
1389) -> Result<(), TrackError> {
1390 validate_covariance_matrix(matrix, dimension, field)?;
1391 let flat = flatten(matrix)?;
1392 let mut scratch = FlatCholeskySolveScratch::default();
1393 let rhs = vec![0.0; dimension];
1394 solve_flat_normal_square_root_into(&flat, &rhs, &mut scratch)
1395 .map(|_| ())
1396 .ok_or(TrackError::NonPositiveDefinite { field })
1397}
1398
1399fn map_field_error(error: FieldError) -> TrackError {
1400 invalid_input(error.field(), error.reason())
1401}
1402
1403fn invalid_input(field: &'static str, reason: &'static str) -> TrackError {
1404 TrackError::InvalidInput { field, reason }
1405}
1406
1407fn transpose(matrix: &[Vec<f64>]) -> Result<Vec<Vec<f64>>, TrackError> {
1408 if matrix.is_empty() {
1409 return Err(invalid_input("matrix", "must not be empty"));
1410 }
1411 let rows = matrix.len();
1412 let cols = matrix[0].len();
1413 if cols == 0 {
1414 return Err(invalid_input("matrix", "must not be empty"));
1415 }
1416 for row in matrix {
1417 validate_vector_len(row, cols, "matrix")?;
1418 }
1419 let mut out = vec![vec![0.0; rows]; cols];
1420 for row in 0..rows {
1421 for col in 0..cols {
1422 out[col][row] = matrix[row][col];
1423 }
1424 }
1425 Ok(out)
1426}
1427
1428fn matvec(matrix: &[Vec<f64>], vector: &[f64]) -> Result<Vec<f64>, TrackError> {
1429 if matrix.is_empty() {
1430 return Err(invalid_input("matrix", "must not be empty"));
1431 }
1432 let cols = vector.len();
1433 if cols == 0 {
1434 return Err(invalid_input("vector", "must not be empty"));
1435 }
1436 for row in matrix {
1437 validate_vector_len(row, cols, "matrix")?;
1438 }
1439 validate_vector_len(vector, cols, "vector")?;
1440 let mut out = vec![0.0; matrix.len()];
1441 for row in 0..matrix.len() {
1442 for (col, value) in vector.iter().enumerate() {
1443 out[row] += matrix[row][col] * value;
1444 }
1445 }
1446 validate_vector_len(&out, matrix.len(), "matrix_vector_product")?;
1447 Ok(out)
1448}
1449
1450fn matmul(a: &[Vec<f64>], b: &[Vec<f64>]) -> Result<Vec<Vec<f64>>, TrackError> {
1451 if a.is_empty() || b.is_empty() {
1452 return Err(invalid_input("matrix", "must not be empty"));
1453 }
1454 let inner = a[0].len();
1455 if inner == 0 {
1456 return Err(invalid_input("matrix", "must not be empty"));
1457 }
1458 for row in a {
1459 validate_vector_len(row, inner, "matrix_a")?;
1460 }
1461 if b.len() != inner {
1462 return Err(TrackError::DimensionMismatch {
1463 field: "matrix_b",
1464 expected: inner,
1465 actual: b.len(),
1466 });
1467 }
1468 let cols = b[0].len();
1469 if cols == 0 {
1470 return Err(invalid_input("matrix_b", "must not be empty"));
1471 }
1472 for row in b {
1473 validate_vector_len(row, cols, "matrix_b")?;
1474 }
1475 let mut out = vec![vec![0.0; cols]; a.len()];
1476 for row in 0..a.len() {
1477 for col in 0..cols {
1478 for k in 0..inner {
1479 out[row][col] += a[row][k] * b[k][col];
1480 }
1481 }
1482 }
1483 for row in &out {
1484 validate_vector_len(row, cols, "matrix_product")?;
1485 }
1486 Ok(out)
1487}
1488
1489fn matrix_add(a: &[Vec<f64>], b: &[Vec<f64>]) -> Result<Vec<Vec<f64>>, TrackError> {
1490 validate_same_matrix_size(a, b, "matrix_add")?;
1491 let mut out = vec![vec![0.0; a[0].len()]; a.len()];
1492 for row in 0..a.len() {
1493 for col in 0..a[0].len() {
1494 out[row][col] = a[row][col] + b[row][col];
1495 }
1496 }
1497 Ok(out)
1498}
1499
1500fn matrix_sub(a: &[Vec<f64>], b: &[Vec<f64>]) -> Result<Vec<Vec<f64>>, TrackError> {
1501 validate_same_matrix_size(a, b, "matrix_sub")?;
1502 let mut out = vec![vec![0.0; a[0].len()]; a.len()];
1503 for row in 0..a.len() {
1504 for col in 0..a[0].len() {
1505 out[row][col] = a[row][col] - b[row][col];
1506 }
1507 }
1508 Ok(out)
1509}
1510
1511fn vector_sub(a: &[f64], b: &[f64], field: &'static str) -> Result<Vec<f64>, TrackError> {
1512 if a.len() != b.len() {
1513 return Err(TrackError::DimensionMismatch {
1514 field,
1515 expected: a.len(),
1516 actual: b.len(),
1517 });
1518 }
1519 validate_vector_len(a, a.len(), field)?;
1520 validate_vector_len(b, b.len(), field)?;
1521 let out = a
1522 .iter()
1523 .zip(b)
1524 .map(|(lhs, rhs)| lhs - rhs)
1525 .collect::<Vec<_>>();
1526 validate_vector_len(&out, a.len(), field)?;
1527 Ok(out)
1528}
1529
1530fn validate_same_matrix_size(
1531 a: &[Vec<f64>],
1532 b: &[Vec<f64>],
1533 field: &'static str,
1534) -> Result<(), TrackError> {
1535 if a.is_empty() || b.is_empty() {
1536 return Err(invalid_input(field, "must not be empty"));
1537 }
1538 if a.len() != b.len() {
1539 return Err(TrackError::DimensionMismatch {
1540 field,
1541 expected: a.len(),
1542 actual: b.len(),
1543 });
1544 }
1545 let cols = a[0].len();
1546 if cols == 0 {
1547 return Err(invalid_input(field, "must not be empty"));
1548 }
1549 for row in a {
1550 validate_vector_len(row, cols, field)?;
1551 }
1552 for row in b {
1553 validate_vector_len(row, cols, field)?;
1554 }
1555 Ok(())
1556}
1557
1558fn flatten(matrix: &[Vec<f64>]) -> Result<Vec<f64>, TrackError> {
1559 if matrix.is_empty() {
1560 return Err(invalid_input("matrix", "must not be empty"));
1561 }
1562 let cols = matrix[0].len();
1563 for row in matrix {
1564 validate_vector_len(row, cols, "matrix")?;
1565 }
1566 let mut out = Vec::with_capacity(matrix.len() * cols);
1567 for row in matrix {
1568 out.extend(row);
1569 }
1570 Ok(out)
1571}
1572
1573fn copy_lower_to_upper(matrix: &mut [Vec<f64>]) {
1574 let dimension = matrix.len();
1575 for row in 0..dimension {
1576 let (head, tail) = matrix.split_at_mut(row + 1);
1577 let row_values = &mut head[row];
1578 for (offset, lower_row) in tail.iter().enumerate() {
1579 let col = row + 1 + offset;
1580 row_values[col] = lower_row[row];
1581 }
1582 }
1583}
1584
1585#[cfg(test)]
1586mod tests {
1587 use super::*;
1596 use crate::astro::math::linear::invert_symmetric_pd;
1597 use crate::estimation::kalman_cv_steady_state_gains;
1598
1599 const TOL: f64 = 1.0e-12;
1600
1601 #[test]
1602 fn scalar_one_step_matches_direct_cv_recurrence_bits() {
1603 let dt_s: f64 = 0.75;
1604 let q: f64 = 0.125;
1605 let measurement_variance: f64 = 0.8;
1606 let p00: f64 = 3.0;
1607 let p01: f64 = 0.25;
1608 let p11: f64 = 2.0;
1609 let x0: f64 = 4.0;
1610 let v0: f64 = -0.5;
1611 let observation_delta: f64 = 1.3;
1612 let mut filter = TrackFilter::from_position_velocity(
1613 TrackCoordinateFrame::CallerDefinedCartesian,
1614 0.0,
1615 vec![x0],
1616 vec![v0],
1617 vec![vec![p00, p01], vec![p01, p11]],
1618 q,
1619 )
1620 .unwrap();
1621
1622 let q00 = q * dt_s * dt_s * dt_s / 3.0;
1623 let q01 = q * dt_s * dt_s / 2.0;
1624 let q11 = q * dt_s;
1625 let predicted_position = x0 + dt_s * v0;
1626 let p00_pred = p00 + 2.0 * dt_s * p01 + dt_s * dt_s * p11 + q00;
1627 let p01_pred = p01 + dt_s * p11 + q01;
1628 let p11_pred = p11 + q11;
1629 let s = p00_pred + measurement_variance;
1630 let k0 = p00_pred / s;
1631 let k1 = p01_pred / s;
1632
1633 let observation = predicted_position + observation_delta;
1634 let innovation = observation - predicted_position;
1635
1636 filter.predict(dt_s).unwrap();
1637 let update = filter
1638 .update_position(&[observation], &[vec![measurement_variance]])
1639 .unwrap();
1640
1641 assert_eq!(update.kalman_gain[0][0].to_bits(), k0.to_bits());
1642 assert_eq!(update.kalman_gain[1][0].to_bits(), k1.to_bits());
1643 assert_eq!(
1644 update.updated.position_m[0].to_bits(),
1645 (predicted_position + k0 * innovation).to_bits()
1646 );
1647 assert_eq!(
1648 update.updated.velocity_m_s[0].to_bits(),
1649 (v0 + k1 * innovation).to_bits()
1650 );
1651 assert_eq!(
1652 update.updated.covariance[0][0].to_bits(),
1653 (p00_pred - k0 * p00_pred).to_bits()
1654 );
1655 assert_eq!(
1656 update.updated.covariance[0][1].to_bits(),
1657 (p01_pred - k1 * p00_pred).to_bits()
1658 );
1659 assert_eq!(
1660 update.updated.covariance[1][0].to_bits(),
1661 (p01_pred - k1 * p00_pred).to_bits()
1662 );
1663 assert_eq!(
1664 update.updated.covariance[1][1].to_bits(),
1665 (p11_pred - k1 * p01_pred).to_bits()
1666 );
1667 }
1668
1669 #[test]
1670 fn scalar_cv_gains_match_estimation_primitives() {
1671 let tracking_index: f64 = 4.0;
1672 let dt_s: f64 = 1.0;
1673 let measurement_variance: f64 = 1.0;
1674 let q = tracking_index * measurement_variance / dt_s.powi(3);
1675 let expected =
1676 kalman_cv_steady_state_gains(tracking_index, dt_s, measurement_variance).unwrap();
1677 let mut filter = TrackFilter::from_position_velocity(
1678 TrackCoordinateFrame::CallerDefinedCartesian,
1679 0.0,
1680 vec![0.0],
1681 vec![0.0],
1682 vec![
1683 vec![measurement_variance, 0.0],
1684 vec![0.0, measurement_variance],
1685 ],
1686 q,
1687 )
1688 .unwrap();
1689 let observation_covariance = vec![vec![measurement_variance]];
1690 let mut last_gain = Vec::new();
1691 for _ in 0..5_000 {
1692 filter.predict(dt_s).unwrap();
1693 let update = filter
1694 .update_position(&[0.0], &observation_covariance)
1695 .unwrap();
1696 last_gain = update.kalman_gain;
1697 }
1698 filter.predict(dt_s).unwrap();
1699 let update = filter
1700 .update_position(&[0.0], &observation_covariance)
1701 .unwrap();
1702 assert!(
1703 (last_gain[0][0] - update.kalman_gain[0][0]).abs() <= TOL,
1704 "position gain did not settle"
1705 );
1706 assert!(
1707 (last_gain[1][0] - update.kalman_gain[1][0]).abs() <= TOL,
1708 "velocity gain did not settle"
1709 );
1710 assert!(
1711 (update.kalman_gain[0][0] - expected.position_gain).abs() <= TOL,
1712 "position gain mismatch: got {}, expected {}",
1713 update.kalman_gain[0][0],
1714 expected.position_gain
1715 );
1716 assert!(
1717 (update.kalman_gain[1][0] - expected.rate_gain).abs() <= TOL,
1718 "velocity gain mismatch: got {}, expected {}",
1719 update.kalman_gain[1][0],
1720 expected.rate_gain
1721 );
1722 }
1723
1724 #[test]
1725 fn covariance_weighting_limits_wide_outlier_motion() {
1726 let mut filter = TrackFilter::from_position_velocity(
1727 TrackCoordinateFrame::CallerDefinedCartesian,
1728 0.0,
1729 vec![10.0],
1730 vec![1.0],
1731 vec![vec![4.0, 0.0], vec![0.0, 1.0]],
1732 0.0,
1733 )
1734 .unwrap();
1735 filter.predict(1.0).unwrap();
1736 let predicted_position = filter.state().position_m[0];
1737 let predicted_covariance = filter.state().covariance.clone();
1738 let wide_variance = 1.0e6;
1739 let outlier = predicted_position + 500.0;
1740 let update = filter
1741 .update_position(&[outlier], &[vec![wide_variance]])
1742 .unwrap();
1743
1744 let s = predicted_covariance[0][0] + wide_variance;
1745 let expected_position_gain = predicted_covariance[0][0] / s;
1746 let expected_velocity_gain = predicted_covariance[1][0] / s;
1747 let innovation = outlier - predicted_position;
1748 assert_eq!(update.innovation.innovation, vec![innovation]);
1749 assert!(
1750 (update.kalman_gain[0][0] - expected_position_gain).abs() <= TOL,
1751 "position gain mismatch"
1752 );
1753 assert!(
1754 (update.kalman_gain[1][0] - expected_velocity_gain).abs() <= TOL,
1755 "velocity gain mismatch"
1756 );
1757 assert!(
1758 (update.updated.position_m[0]
1759 - (predicted_position + expected_position_gain * innovation))
1760 .abs()
1761 <= TOL,
1762 "position update mismatch"
1763 );
1764 assert!(
1765 (update.updated.position_m[0] - predicted_position).abs() < 0.01,
1766 "wide covariance outlier moved the track too far"
1767 );
1768 }
1769
1770 #[test]
1771 fn tight_covariance_observation_pulls_track_more_than_wide_one() {
1772 let base = TrackFilter::from_position_velocity(
1773 TrackCoordinateFrame::CallerDefinedCartesian,
1774 0.0,
1775 vec![0.0],
1776 vec![0.0],
1777 vec![vec![9.0, 0.0], vec![0.0, 1.0]],
1778 0.0,
1779 )
1780 .unwrap();
1781 let mut wide = base.clone();
1782 let mut tight = base;
1783 let wide_update = wide.update_position(&[10.0], &[vec![10_000.0]]).unwrap();
1784 let tight_update = tight.update_position(&[10.0], &[vec![1.0]]).unwrap();
1785
1786 assert!(wide_update.kalman_gain[0][0] < 0.001);
1787 assert!(tight_update.kalman_gain[0][0] > 0.8);
1788 assert!(wide_update.updated.position_m[0] < 0.01);
1789 assert!(tight_update.updated.position_m[0] > 8.0);
1790 }
1791
1792 #[test]
1793 fn rts_smoother_matches_closed_form_two_epoch_recursion() {
1794 let mut filter = TrackFilter::from_position_velocity(
1795 TrackCoordinateFrame::CallerDefinedCartesian,
1796 0.0,
1797 vec![0.0],
1798 vec![1.0],
1799 vec![vec![2.0, 0.2], vec![0.2, 1.0]],
1800 0.5,
1801 )
1802 .unwrap();
1803 let mut history = TrackRtsHistoryBuilder::from_filter(&filter).unwrap();
1804 filter.predict_recorded(1.0, &mut history).unwrap();
1805 filter
1806 .update_position_recorded(&[1.2], &[vec![0.25]], &mut history)
1807 .unwrap();
1808 let history = history.finish().unwrap();
1809 let smoothed = rts_smooth(&history).unwrap();
1810
1811 let first = &history.epochs[0];
1812 let second = &history.epochs[1];
1813 let transition = second.transition_from_previous.as_ref().unwrap();
1814 let expected_gain = closed_form_rts_gain(
1815 &first.updated.covariance,
1816 transition,
1817 &second.predicted.covariance,
1818 );
1819 let expected_delta = vector_sub(
1820 &second.updated.state_vector(),
1821 &second.predicted.state_vector(),
1822 "delta",
1823 )
1824 .unwrap();
1825 let expected_correction = matvec(&expected_gain, &expected_delta).unwrap();
1826 let expected_state = first
1827 .updated
1828 .state_vector()
1829 .iter()
1830 .zip(expected_correction)
1831 .map(|(value, correction)| value + correction)
1832 .collect::<Vec<_>>();
1833 let expected_covariance = smoothed_covariance(
1834 &first.updated.covariance,
1835 &expected_gain,
1836 &second.predicted.covariance,
1837 &second.updated.covariance,
1838 )
1839 .unwrap();
1840
1841 let got_first = &smoothed.epochs[0];
1842 assert_close_vec(&got_first.state.state_vector(), &expected_state, TOL);
1843 assert_close_matrix(
1844 &got_first.rts_gain_to_next.clone().unwrap(),
1845 &expected_gain,
1846 TOL,
1847 );
1848 assert_close_matrix(&got_first.state.covariance, &expected_covariance, TOL);
1849 assert_eq!(smoothed.epochs[1].state, second.updated);
1850 }
1851
1852 #[test]
1853 fn smoothing_covariance_does_not_exceed_filtering_covariance_diagonal() {
1854 let mut filter = TrackFilter::from_position_velocity(
1855 TrackCoordinateFrame::CallerDefinedCartesian,
1856 0.0,
1857 vec![0.0],
1858 vec![0.0],
1859 vec![vec![10.0, 0.0], vec![0.0, 10.0]],
1860 0.1,
1861 )
1862 .unwrap();
1863 let mut history = TrackRtsHistoryBuilder::from_filter(&filter).unwrap();
1864 for (idx, observation) in [0.1, 0.9, 2.2, 3.1].iter().enumerate() {
1865 filter.predict_recorded(1.0, &mut history).unwrap();
1866 if idx == 2 {
1867 filter.record_prediction_only(&mut history).unwrap();
1868 } else {
1869 filter
1870 .update_position_recorded(&[*observation], &[vec![0.5]], &mut history)
1871 .unwrap();
1872 }
1873 }
1874 let history = history.finish().unwrap();
1875 let smoothed = rts_smooth(&history).unwrap();
1876 assert_eq!(smoothed.epochs.len(), history.epochs.len());
1877 for (smoothed_epoch, filtered_epoch) in smoothed.epochs.iter().zip(&history.epochs) {
1878 for idx in 0..smoothed_epoch.state.state_dimension() {
1879 assert!(
1880 smoothed_epoch.state.covariance[idx][idx]
1881 <= filtered_epoch.updated.covariance[idx][idx] + 1.0e-10,
1882 "smoothed covariance diagonal exceeded filtered covariance"
1883 );
1884 }
1885 }
1886 }
1887
1888 #[test]
1889 fn gated_rejection_records_prediction_only_epoch() {
1890 let mut filter = TrackFilter::from_position_velocity(
1891 TrackCoordinateFrame::CallerDefinedCartesian,
1892 0.0,
1893 vec![0.0],
1894 vec![1.0],
1895 vec![vec![1.0, 0.0], vec![0.0, 1.0]],
1896 0.1,
1897 )
1898 .unwrap();
1899 let mut history = TrackRtsHistoryBuilder::from_filter(&filter).unwrap();
1900 filter.predict_recorded(1.0, &mut history).unwrap();
1901 let predicted = filter.state().clone();
1902 let gated = filter
1903 .update_position_gated_recorded(&[100.0], &[vec![0.01]], 0.95, &mut history)
1904 .unwrap();
1905 assert!(!gated.gate.in_gate);
1906 assert!(gated.update.is_none());
1907 assert_eq!(*filter.state(), predicted);
1908 let history = history.finish().unwrap();
1909 assert_eq!(history.epochs.len(), 2);
1910 assert_eq!(history.epochs[1].predicted, predicted);
1911 assert_eq!(history.epochs[1].updated, predicted);
1912 }
1913
1914 #[test]
1915 fn position_covariance_type_selects_frame_block() {
1916 let covariance = PositionCovariance {
1917 ecef_m2: [[1.0, 0.0, 0.0], [0.0, 2.0, 0.0], [0.0, 0.0, 3.0]],
1918 enu_m2: [[4.0, 0.0, 0.0], [0.0, 5.0, 0.0], [0.0, 0.0, 6.0]],
1919 };
1920 let mut ecef = TrackFilter::from_position3(
1921 TrackCoordinateFrame::Ecef,
1922 0.0,
1923 [0.0, 0.0, 0.0],
1924 [[10.0, 0.0, 0.0], [0.0, 10.0, 0.0], [0.0, 0.0, 10.0]],
1925 1.0,
1926 0.0,
1927 )
1928 .unwrap();
1929 let update = ecef
1930 .update_position_covariance([1.0, 0.0, 0.0], &covariance)
1931 .unwrap();
1932 assert!((update.kalman_gain[0][0] - 10.0 / 11.0).abs() <= TOL);
1933
1934 let mut enu = TrackFilter::from_position3(
1935 TrackCoordinateFrame::Enu,
1936 0.0,
1937 [0.0, 0.0, 0.0],
1938 [[10.0, 0.0, 0.0], [0.0, 10.0, 0.0], [0.0, 0.0, 10.0]],
1939 1.0,
1940 0.0,
1941 )
1942 .unwrap();
1943 let update = enu
1944 .update_position_covariance([1.0, 0.0, 0.0], &covariance)
1945 .unwrap();
1946 assert!((update.kalman_gain[0][0] - 10.0 / 14.0).abs() <= TOL);
1947 }
1948
1949 fn closed_form_rts_gain(
1950 filtered_covariance: &[Vec<f64>],
1951 transition: &[Vec<f64>],
1952 predicted_covariance_next: &[Vec<f64>],
1953 ) -> Vec<Vec<f64>> {
1954 let transition_t = transpose(transition).unwrap();
1955 let cross = matmul(filtered_covariance, &transition_t).unwrap();
1956 let inverse = invert_symmetric_pd(predicted_covariance_next).unwrap();
1957 matmul(&cross, &inverse).unwrap()
1958 }
1959
1960 fn assert_close_vec(got: &[f64], expected: &[f64], tolerance: f64) {
1961 assert_eq!(got.len(), expected.len());
1962 for (lhs, rhs) in got.iter().zip(expected) {
1963 assert!(
1964 (lhs - rhs).abs() <= tolerance,
1965 "vector mismatch: got {lhs}, expected {rhs}"
1966 );
1967 }
1968 }
1969
1970 fn assert_close_matrix(got: &[Vec<f64>], expected: &[Vec<f64>], tolerance: f64) {
1971 assert_eq!(got.len(), expected.len());
1972 for (row_lhs, row_rhs) in got.iter().zip(expected) {
1973 assert_close_vec(row_lhs, row_rhs, tolerance);
1974 }
1975 }
1976}