1use crate::linalg::allocator::Allocator;
20use crate::linalg::{DefaultAllocator, DimName};
21use crate::md::trajectory::{Interpolatable, Traj};
22pub use crate::od::estimate::*;
23pub use crate::od::ground_station::*;
24pub use crate::od::snc::*;
25pub use crate::od::*;
26use crate::propagators::PropInstance;
27pub use crate::time::{Duration, Unit};
28use anise::prelude::Almanac;
29use indexmap::IndexSet;
30use msr::sensitivity::TrackerSensitivity;
31use snafu::prelude::*;
32mod conf;
33pub use conf::{IterationConf, SmoothingArc};
34mod trigger;
35pub use trigger::EkfTrigger;
36mod rejectcrit;
37use self::msr::TrackingDataArc;
38pub use self::rejectcrit::ResidRejectCrit;
39use std::collections::BTreeMap;
40use std::marker::PhantomData;
41use std::ops::Add;
42mod export;
43
44#[allow(clippy::upper_case_acronyms)]
61pub struct ODProcess<
62 'a,
63 D: Dynamics,
64 MsrSize: DimName,
65 Accel: DimName,
66 K: Filter<D::StateType, Accel, MsrSize>,
67 Trk: TrackerSensitivity<D::StateType, D::StateType>,
68> where
69 D::StateType:
70 Interpolatable + Add<OVector<f64, <D::StateType as State>::Size>, Output = D::StateType>,
71 <DefaultAllocator as Allocator<<D::StateType as State>::VecLength>>::Buffer<f64>: Send,
72 DefaultAllocator: Allocator<<D::StateType as State>::Size>
73 + Allocator<<D::StateType as State>::VecLength>
74 + Allocator<MsrSize>
75 + Allocator<MsrSize, <D::StateType as State>::Size>
76 + Allocator<MsrSize, MsrSize>
77 + Allocator<<D::StateType as State>::Size, <D::StateType as State>::Size>
78 + Allocator<Accel>
79 + Allocator<Accel, Accel>
80 + Allocator<<D::StateType as State>::Size, Accel>
81 + Allocator<Accel, <D::StateType as State>::Size>,
82{
83 pub prop: PropInstance<'a, D>,
85 pub kf: K,
87 pub devices: BTreeMap<String, Trk>,
89 pub estimates: Vec<K::Estimate>,
91 pub residuals: Vec<Option<Residual<MsrSize>>>,
93 pub ekf_trigger: Option<EkfTrigger>,
94 pub resid_crit: Option<ResidRejectCrit>,
96 pub almanac: Arc<Almanac>,
97 init_state: D::StateType,
98 _marker: PhantomData<Accel>,
99}
100
101impl<
102 'a,
103 D: Dynamics,
104 MsrSize: DimName,
105 Accel: DimName,
106 K: Filter<D::StateType, Accel, MsrSize>,
107 Trk: TrackerSensitivity<D::StateType, D::StateType>,
108 > ODProcess<'a, D, MsrSize, Accel, K, Trk>
109where
110 D::StateType:
111 Interpolatable + Add<OVector<f64, <D::StateType as State>::Size>, Output = D::StateType>,
112 <DefaultAllocator as Allocator<<D::StateType as State>::VecLength>>::Buffer<f64>: Send,
113 DefaultAllocator: Allocator<<D::StateType as State>::Size>
114 + Allocator<<D::StateType as State>::VecLength>
115 + Allocator<MsrSize>
116 + Allocator<MsrSize, <D::StateType as State>::Size>
117 + Allocator<MsrSize, MsrSize>
118 + Allocator<<D::StateType as State>::Size, <D::StateType as State>::Size>
119 + Allocator<Accel>
120 + Allocator<Accel, Accel>
121 + Allocator<<D::StateType as State>::Size, Accel>
122 + Allocator<Accel, <D::StateType as State>::Size>,
123{
124 pub fn new(
126 prop: PropInstance<'a, D>,
127 kf: K,
128 devices: BTreeMap<String, Trk>,
129 ekf_trigger: Option<EkfTrigger>,
130 resid_crit: Option<ResidRejectCrit>,
131 almanac: Arc<Almanac>,
132 ) -> Self {
133 let init_state = prop.state;
134 Self {
135 prop: prop.quiet(),
136 kf,
137 devices,
138 estimates: Vec::with_capacity(10_000),
139 residuals: Vec::with_capacity(10_000),
140 ekf_trigger,
141 resid_crit,
142 almanac,
143 init_state,
144 _marker: PhantomData::<Accel>,
145 }
146 }
147
148 pub fn ekf(
150 prop: PropInstance<'a, D>,
151 kf: K,
152 devices: BTreeMap<String, Trk>,
153 trigger: EkfTrigger,
154 resid_crit: Option<ResidRejectCrit>,
155 almanac: Arc<Almanac>,
156 ) -> Self {
157 let init_state = prop.state;
158 Self {
159 prop: prop.quiet(),
160 kf,
161 devices,
162 estimates: Vec::with_capacity(10_000),
163 residuals: Vec::with_capacity(10_000),
164 ekf_trigger: Some(trigger),
165 resid_crit,
166 almanac,
167 init_state,
168 _marker: PhantomData::<Accel>,
169 }
170 }
171
172 pub fn smooth(&self, condition: SmoothingArc) -> Result<Vec<K::Estimate>, ODError> {
177 let l = self.estimates.len() - 1;
178
179 info!("Smoothing {} estimates until {}", l + 1, condition);
180 let mut smoothed = Vec::with_capacity(self.estimates.len());
181 smoothed.push(self.estimates.last().unwrap().clone());
183
184 loop {
185 let k = l - smoothed.len();
186 let sm_est_kp1 = &self.estimates[k + 1];
188 let x_kp1_l = sm_est_kp1.state_deviation();
189 let p_kp1_l = sm_est_kp1.covar();
190 let est_k = &self.estimates[k];
192 let est_kp1 = &self.estimates[k + 1];
194
195 match condition {
197 SmoothingArc::Epoch(e) => {
198 if est_kp1.epoch() < e {
200 break;
201 }
202 }
203 SmoothingArc::TimeGap(gap_s) => {
204 if est_kp1.epoch() - est_k.epoch() > gap_s {
205 break;
206 }
207 }
208 SmoothingArc::Prediction => {
209 if est_kp1.predicted() {
210 break;
211 }
212 }
213 SmoothingArc::All => {}
214 }
215
216 let phi_kp1_k = &est_kp1
222 .stm()
223 .clone()
224 .try_inverse()
225 .ok_or(ODError::SingularStateTransitionMatrix)?;
226
227 let x_k_l = phi_kp1_k * x_kp1_l;
229 let p_k_l = phi_kp1_k * p_kp1_l * phi_kp1_k.transpose();
231 let mut smoothed_est_k = est_k.clone();
233 smoothed_est_k.set_state_deviation(x_k_l);
235 smoothed_est_k.set_covar(p_k_l);
237
238 smoothed.push(smoothed_est_k);
240 if smoothed.len() == self.estimates.len() {
241 break;
242 }
243 }
244
245 info!(
247 "Smoothed {} estimates (from {} to {})",
248 smoothed.len(),
249 smoothed.last().unwrap().epoch(),
250 smoothed[0].epoch(),
251 );
252
253 if smoothed.len() < self.estimates.len() {
256 let mut k = self.estimates.len() - smoothed.len();
258 loop {
259 smoothed.push(self.estimates[k].clone());
260 if k == 0 {
261 break;
262 }
263 k -= 1;
264 }
265 }
266
267 smoothed.reverse();
269
270 Ok(smoothed)
271 }
272
273 pub fn rms_residual_ratios(&self) -> f64 {
275 let mut sum = 0.0;
276 for residual in self.residuals.iter().flatten() {
277 sum += residual.ratio.powi(2);
278 }
279 (sum / (self.residuals.len() as f64)).sqrt()
280 }
281
282 pub fn iterate_arc(
284 &mut self,
285 arc: &TrackingDataArc,
286 config: IterationConf,
287 ) -> Result<(), ODError> {
288 let mut best_rms = self.rms_residual_ratios();
289 let mut previous_rms = best_rms;
290 let mut divergence_cnt = 0;
291 let mut iter_cnt = 0;
292 loop {
293 if best_rms <= config.absolute_tol {
294 info!("*****************");
295 info!("*** CONVERGED ***");
296 info!("*****************");
297
298 info!(
299 "Filter converged to absolute tolerance ({:.2e} < {:.2e}) after {} iterations",
300 best_rms, config.absolute_tol, iter_cnt
301 );
302 break;
303 }
304
305 iter_cnt += 1;
306
307 if let Some(trigger) = &mut self.ekf_trigger {
309 trigger.reset();
310 }
311
312 info!("***************************");
313 info!("*** Iteration number {iter_cnt:02} ***");
314 info!("***************************");
315
316 let smoothed = self.smooth(config.smoother)?;
318 self.prop.state = self.init_state;
320 self.estimates = Vec::with_capacity(arc.measurements.len().max(self.estimates.len()));
322 self.residuals = Vec::with_capacity(arc.measurements.len().max(self.estimates.len()));
323
324 self.kf.set_previous_estimate(&smoothed[0]);
325 self.process_arc(arc)?;
327
328 let new_rms = self.rms_residual_ratios();
330 let cur_rms_num = (new_rms - previous_rms).abs();
331 let cur_rel_rms = cur_rms_num / previous_rms;
332 if cur_rel_rms < config.relative_tol || cur_rms_num < config.absolute_tol * best_rms {
333 if previous_rms < best_rms {
334 best_rms = previous_rms;
335 }
336 info!("*****************");
337 info!("*** CONVERGED ***");
338 info!("*****************");
339 info!(
340 "New residual RMS: {:.5}\tPrevious RMS: {:.5}\tBest RMS: {:.5}",
341 new_rms, previous_rms, best_rms
342 );
343 if cur_rel_rms < config.relative_tol {
344 info!(
345 "Filter converged on relative tolerance ({:.2e} < {:.2e}) after {} iterations",
346 cur_rel_rms, config.relative_tol, iter_cnt
347 );
348 } else {
349 info!(
350 "Filter converged on relative change ({:.2e} < {:.2e} * {:.2e}) after {} iterations",
351 cur_rms_num, config.absolute_tol, best_rms, iter_cnt
352 );
353 }
354 break;
355 } else if new_rms > previous_rms {
356 warn!(
357 "New residual RMS: {:.5}\tPrevious RMS: {:.5}\tBest RMS: {:.5} ({cur_rel_rms:.2e} > {:.2e})",
358 new_rms, previous_rms, best_rms, config.relative_tol
359 );
360 divergence_cnt += 1;
361 previous_rms = new_rms;
362 if divergence_cnt >= config.max_divergences {
363 let msg = format!(
364 "Filter iterations have continuously diverged {} times: {}",
365 config.max_divergences, config
366 );
367 if config.force_failure {
368 return Err(ODError::Diverged {
369 loops: config.max_divergences,
370 });
371 } else {
372 error!("{}", msg);
373 break;
374 }
375 } else {
376 warn!("Filter iteration caused divergence {} of {} acceptable subsequent divergences", divergence_cnt, config.max_divergences);
377 }
378 } else {
379 info!(
380 "New residual RMS: {:.5}\tPrevious RMS: {:.5}\tBest RMS: {:.5} ({cur_rel_rms:.2e} > {:.2e})",
381 new_rms, previous_rms, best_rms, config.relative_tol
382 );
383 divergence_cnt = 0;
385 previous_rms = new_rms;
386 if previous_rms < best_rms {
387 best_rms = previous_rms;
388 }
389 }
390
391 if iter_cnt >= config.max_iterations {
392 let msg = format!(
393 "Filter has iterated {} times but failed to reach filter convergence criteria: {}",
394 config.max_iterations, config
395 );
396 if config.force_failure {
397 return Err(ODError::Diverged {
398 loops: config.max_divergences,
399 });
400 } else {
401 error!("{}", msg);
402 break;
403 }
404 }
405 }
406
407 Ok(())
408 }
409
410 #[allow(clippy::erasing_op)]
417 pub fn process_arc(&mut self, arc: &TrackingDataArc) -> Result<(), ODError> {
418 let measurements = &arc.measurements;
419 ensure!(
420 measurements.len() >= 2,
421 TooFewMeasurementsSnafu {
422 need: 2_usize,
423 action: "running a Kalman filter"
424 }
425 );
426
427 let max_step = match arc.min_duration_sep() {
428 Some(step_size) => step_size,
429 None => {
430 return Err(ODError::TooFewMeasurements {
431 action: "determining the minimum step size",
432 need: 2,
433 })
434 }
435 };
436
437 ensure!(
438 !max_step.is_negative() && max_step != Duration::ZERO,
439 StepSizeSnafu { step: max_step }
440 );
441
442 if MsrSize::USIZE > arc.unique_types().len() {
444 error!("Filter misconfigured: expect high rejection count!");
445 error!(
446 "Arc only contains {} measurement types, but filter configured for {}.",
447 arc.unique_types().len(),
448 MsrSize::USIZE
449 );
450 error!("Filter should be configured for these numbers to match.");
451 error!("Consider running subsequent arcs if ground stations provide different measurements.")
452 }
453
454 let num_msrs = measurements.len();
456
457 if !self.prop.fixed_step {
459 self.prop.set_step(max_step, false);
460 }
461
462 let prop_time = arc.end_epoch().unwrap() - self.kf.previous_estimate().epoch();
463 info!("Navigation propagating for a total of {prop_time} with step size {max_step}");
464
465 let mut epoch = self.prop.state.epoch();
466
467 let mut reported = [false; 11];
468 reported[0] = true; info!("Processing {num_msrs} measurements with covariance mapping");
470
471 let mut traj: Traj<D::StateType> = Traj::new();
473
474 let mut msr_accepted_cnt: usize = 0;
475 let tick = Epoch::now().unwrap();
476
477 for (msr_cnt, (epoch_ref, msr)) in measurements.iter().enumerate() {
478 let next_msr_epoch = *epoch_ref;
479
480 loop {
482 let delta_t = next_msr_epoch - epoch;
483
484 let next_step_size = delta_t.min(self.prop.step_size).min(max_step);
486
487 let mut index = traj.states.len();
490 while index > 0 {
491 index -= 1;
492 if traj.states[index].epoch() >= epoch {
493 break;
494 }
495 }
496 traj.states.truncate(index);
497
498 debug!("propagate for {next_step_size} (Δt to next msr: {delta_t})");
499 let (_, traj_covar) = self
500 .prop
501 .for_duration_with_traj(next_step_size)
502 .context(ODPropSnafu)?;
503
504 for state in traj_covar.states {
505 traj.states.push(state);
508 }
509
510 let nominal_state = self.prop.state;
514 epoch = nominal_state.epoch();
516
517 if nominal_state.epoch() == next_msr_epoch {
519 match self.devices.get_mut(&msr.tracker) {
521 Some(device) => {
522 if let Some(computed_meas) =
523 device.measure(epoch, &traj, None, self.almanac.clone())?
524 {
525 let msr_types = device.measurement_types();
526
527 if let Some(trigger) = &mut self.ekf_trigger {
529 if self.kf.is_extended() && trigger.disable_ekf(epoch) {
530 self.kf.set_extended(false);
531 info!("EKF disabled @ {epoch}");
532 }
533 }
534
535 let windows = msr_types.len() / MsrSize::USIZE;
537 let mut msr_rejected = false;
538 for wno in 0..=windows {
539 let mut cur_msr_types = IndexSet::new();
540 for msr_type in msr_types
541 .iter()
542 .copied()
543 .skip(wno * MsrSize::USIZE)
544 .take(MsrSize::USIZE)
545 {
546 cur_msr_types.insert(msr_type);
547 }
548
549 if cur_msr_types.is_empty() {
550 break;
552 }
553
554 for val in
556 msr.observation::<MsrSize>(&cur_msr_types).iter().copied()
557 {
558 ensure!(
559 val.is_finite(),
560 InvalidMeasurementSnafu {
561 epoch: next_msr_epoch,
562 val
563 }
564 );
565 }
566
567 let h_tilde = device
568 .h_tilde::<MsrSize>(
569 msr,
570 &cur_msr_types,
571 &nominal_state,
572 self.almanac.clone(),
573 )
574 .unwrap();
575
576 self.kf.update_h_tilde(h_tilde);
577
578 let computed_obs = computed_meas
579 .observation::<MsrSize>(&cur_msr_types)
580 - device.measurement_bias_vector::<MsrSize>(
581 &cur_msr_types,
582 epoch,
583 )?;
584
585 let mut real_obs = msr.observation(&cur_msr_types);
586
587 if let Some(moduli) = &arc.moduli {
588 let mut obs_ambiguity = OVector::<f64, MsrSize>::zeros();
589
590 for (i, msr_type) in cur_msr_types.iter().enumerate() {
591 if let Some(modulus) = moduli.get(msr_type) {
592 let k = computed_obs[i].div_euclid(*modulus);
593 obs_ambiguity[i] = k * *modulus;
595 }
596 }
597 real_obs += obs_ambiguity;
598 }
599
600 match self.kf.measurement_update(
601 nominal_state,
602 &real_obs,
603 &computed_obs,
604 device.measurement_covar_matrix(&cur_msr_types, epoch)?,
605 self.resid_crit,
606 ) {
607 Ok((estimate, mut residual)) => {
608 debug!("processed measurement #{msr_cnt} for {cur_msr_types:?} @ {epoch} from {}", device.name());
609
610 residual.tracker = Some(device.name());
611 residual.msr_types = cur_msr_types;
612
613 if residual.rejected {
614 msr_rejected = true;
615 }
616
617 if let Some(trigger) = &mut self.ekf_trigger {
623 if trigger.enable_ekf(&estimate)
624 && !self.kf.is_extended()
625 {
626 self.kf.set_extended(true);
627 if !estimate.within_3sigma() {
628 warn!("EKF enabled @ {epoch} but filter DIVERGING");
629 } else {
630 info!("EKF enabled @ {epoch}");
631 }
632 }
633 if self.kf.is_extended() {
634 self.prop.state = self.prop.state
635 + estimate.state_deviation();
636 }
637 }
638
639 self.prop.state.reset_stm();
640
641 self.estimates.push(estimate);
642 self.residuals.push(Some(residual));
643 }
644 Err(e) => return Err(e),
645 }
646 }
647 if !msr_rejected {
648 msr_accepted_cnt += 1;
649 }
650 } else {
651 warn!("Ignoring observation @ {epoch} because simulated {} does not expect it", msr.tracker);
652 }
653 }
654 None => {
655 error!(
656 "Tracker {} is not in the list of configured devices",
657 msr.tracker
658 )
659 }
660 }
661
662 let msr_prct = (10.0 * (msr_cnt as f64) / (num_msrs as f64)) as usize;
663 if !reported[msr_prct] {
664 let num_rejected = msr_cnt - msr_accepted_cnt.saturating_sub(1);
665 let msg = format!(
666 "{:>3}% done - {msr_accepted_cnt:.0} measurements accepted, {:.0} rejected",
667 10 * msr_prct, num_rejected
668 );
669 if msr_accepted_cnt < num_rejected {
670 warn!("{msg}");
671 } else {
672 info!("{msg}");
673 }
674 reported[msr_prct] = true;
675 }
676
677 break;
678 } else {
679 debug!("time update {epoch}");
681 match self.kf.time_update(nominal_state) {
682 Ok(est) => {
683 self.estimates.push(est);
686 self.residuals.push(None);
688 }
689 Err(e) => return Err(e),
690 }
691 self.prop.state.reset_stm();
692 }
693 }
694 }
695
696 if !reported[10] {
698 let tock_time = Epoch::now().unwrap() - tick;
699 info!(
700 "100% done - {msr_accepted_cnt:.0} measurements accepted, {:.0} rejected (done in {tock_time})",
701 num_msrs - msr_accepted_cnt
702 );
703 }
704
705 Ok(())
706 }
707
708 pub fn predict_until(&mut self, step: Duration, end_epoch: Epoch) -> Result<(), ODError> {
710 let prop_time = end_epoch - self.kf.previous_estimate().epoch();
711 info!("Mapping covariance for {prop_time} with {step} step");
712
713 loop {
714 let mut epoch = self.prop.state.epoch();
715 if epoch + self.prop.details.step > end_epoch {
716 self.prop.until_epoch(end_epoch).context(ODPropSnafu)?;
717 } else {
718 self.prop.for_duration(step).context(ODPropSnafu)?;
719 }
720
721 let nominal_state = self.prop.state;
725 epoch = nominal_state.epoch();
727 debug!("time update {epoch}");
729 match self.kf.time_update(nominal_state) {
730 Ok(est) => {
731 self.estimates.push(est);
734 self.residuals.push(None);
735 }
736 Err(e) => return Err(e),
737 }
738 self.prop.state.reset_stm();
739 if epoch == end_epoch {
740 break;
741 }
742 }
743
744 Ok(())
745 }
746
747 pub fn predict_for(&mut self, step: Duration, duration: Duration) -> Result<(), ODError> {
749 let end_epoch = self.kf.previous_estimate().epoch() + duration;
750 self.predict_until(step, end_epoch)
751 }
752
753 pub fn to_traj(&self) -> Result<Traj<D::StateType>, NyxError>
755 where
756 DefaultAllocator: Allocator<<D::StateType as State>::VecLength>,
757 {
758 if self.estimates.is_empty() {
759 Err(NyxError::NoStateData {
760 msg: "No navigation trajectory to generate: run the OD process first".to_string(),
761 })
762 } else {
763 Ok(Traj {
764 states: self
765 .estimates
766 .iter()
767 .map(|est| est.nominal_state())
768 .collect(),
769 name: None,
770 })
771 }
772 }
773}
774
775impl<
776 'a,
777 D: Dynamics,
778 MsrSize: DimName,
779 Accel: DimName,
780 K: Filter<D::StateType, Accel, MsrSize>,
781 Trk: TrackerSensitivity<D::StateType, D::StateType>,
782 > ODProcess<'a, D, MsrSize, Accel, K, Trk>
783where
784 D::StateType:
785 Interpolatable + Add<OVector<f64, <D::StateType as State>::Size>, Output = D::StateType>,
786 <DefaultAllocator as Allocator<<D::StateType as State>::VecLength>>::Buffer<f64>: Send,
787 DefaultAllocator: Allocator<<D::StateType as State>::Size>
788 + Allocator<<D::StateType as State>::VecLength>
789 + Allocator<MsrSize>
790 + Allocator<MsrSize, <D::StateType as State>::Size>
791 + Allocator<MsrSize, MsrSize>
792 + Allocator<<D::StateType as State>::Size, <D::StateType as State>::Size>
793 + Allocator<Accel>
794 + Allocator<Accel, Accel>
795 + Allocator<<D::StateType as State>::Size, Accel>
796 + Allocator<Accel, <D::StateType as State>::Size>,
797{
798 pub fn ckf(
799 prop: PropInstance<'a, D>,
800 kf: K,
801 devices: BTreeMap<String, Trk>,
802 resid_crit: Option<ResidRejectCrit>,
803 almanac: Arc<Almanac>,
804 ) -> Self {
805 let init_state = prop.state;
806 Self {
807 prop: prop.quiet(),
808 kf,
809 devices,
810 estimates: Vec::with_capacity(10_000),
811 residuals: Vec::with_capacity(10_000),
812 resid_crit,
813 ekf_trigger: None,
814 init_state,
815 almanac,
816 _marker: PhantomData::<Accel>,
817 }
818 }
819}