nyx_space/od/process/
mod.rs

1/*
2    Nyx, blazing fast astrodynamics
3    Copyright (C) 2018-onwards Christopher Rabotin <christopher.rabotin@gmail.com>
4
5    This program is free software: you can redistribute it and/or modify
6    it under the terms of the GNU Affero General Public License as published
7    by the Free Software Foundation either version 3 of the License, or
8    (at your option) any later version.
9
10    This program is distributed in the hope that it will be useful,
11    but WITHOUT ANY WARRANTY; without even the implied warranty of
12    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13    GNU Affero General Public License for more details.
14
15    You should have received a copy of the GNU Affero General Public License
16    along with this program.  If not, see <https://www.gnu.org/licenses/>.
17*/
18
19use 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/// Sets up an orbit determination process (ODP).
45///
46/// # Algorithm details
47///
48/// ## Classical vs. Extended Kalman filter
49///
50/// In Nyx, an ODP configured in Classical Kalman Filter will track the state deviation compared to the nominal state.
51/// An ODP configured in Extended Kalman Filter mode will update the propagation state on each (accepted) measurement.
52///
53/// The EKF mode requires a "trigger" which switches the filter from a CKF to an EKF. This prevents quick divergence of a filter.
54///
55/// ## Measurement residual ratio and rejection
56///
57/// The measurement residual is a signed scalar, despite ODP being able to process multiple measurements simultaneously.
58/// By default, if a measurement is more than 3 measurement sigmas off, it will be rejected to avoid biasing the filter.
59///
60#[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    /// PropInstance used for the estimation
84    pub prop: PropInstance<'a, D>,
85    /// Kalman filter itself
86    pub kf: K,
87    /// Tracking devices
88    pub devices: BTreeMap<String, Trk>,
89    /// Vector of estimates available after a pass
90    pub estimates: Vec<K::Estimate>,
91    /// Vector of residuals available after a pass
92    pub residuals: Vec<Option<Residual<MsrSize>>>,
93    pub ekf_trigger: Option<EkfTrigger>,
94    /// Residual rejection criteria allows preventing bad measurements from affecting the estimation.
95    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    /// Initialize a new orbit determination process with an optional trigger to switch from a CKF to an EKF.
125    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    /// Initialize a new orbit determination process with an Extended Kalman filter. The switch from a classical KF to an EKF is based on the provided trigger.
149    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    /// Allows to smooth the provided estimates. Returns the smoothed estimates or an error.
173    ///
174    /// Estimates must be ordered in chronological order. This function will smooth the
175    /// estimates from the last in the list to the first one.
176    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        // Set the first item of the smoothed estimates to the last estimate (we cannot smooth the very last estimate)
182        smoothed.push(self.estimates.last().unwrap().clone());
183
184        loop {
185            let k = l - smoothed.len();
186            // Borrow the previously smoothed estimate of the k+1 estimate
187            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            // Borrow the k-th estimate, which we're smoothing with the next estimate
191            let est_k = &self.estimates[k];
192            // Borrow the k+1-th estimate, which we're smoothing with the next estimate
193            let est_kp1 = &self.estimates[k + 1];
194
195            // Check the smoother stopping condition
196            match condition {
197                SmoothingArc::Epoch(e) => {
198                    // If the epoch of the next estimate is _before_ the stopping time, stop smoothing
199                    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            // Compute the STM between both steps taken by the filter
217            // The filter will reset the STM between each estimate it computes, time update or measurement update.
218            // Therefore, the STM is simply the inverse of the one we used previously.
219            // est_kp1 is the estimate that used the STM from time k to time k+1. So the STM stored there
220            // is \Phi_{k \to k+1}. Let's invert that.
221            let phi_kp1_k = &est_kp1
222                .stm()
223                .clone()
224                .try_inverse()
225                .ok_or(ODError::SingularStateTransitionMatrix)?;
226
227            // Compute smoothed state deviation
228            let x_k_l = phi_kp1_k * x_kp1_l;
229            // Compute smoothed covariance
230            let p_k_l = phi_kp1_k * p_kp1_l * phi_kp1_k.transpose();
231            // Store into vector
232            let mut smoothed_est_k = est_k.clone();
233            // Compute the smoothed state deviation
234            smoothed_est_k.set_state_deviation(x_k_l);
235            // Compute the smoothed covariance
236            smoothed_est_k.set_covar(p_k_l);
237
238            // Move on
239            smoothed.push(smoothed_est_k);
240            if smoothed.len() == self.estimates.len() {
241                break;
242            }
243        }
244
245        // Note that we have yet to reverse the list, so we print them backward
246        info!(
247            "Smoothed {} estimates (from {} to {})",
248            smoothed.len(),
249            smoothed.last().unwrap().epoch(),
250            smoothed[0].epoch(),
251        );
252
253        // Now, let's add all of the other estimates so that the same indexing can be done
254        // between all the estimates and the smoothed estimates
255        if smoothed.len() < self.estimates.len() {
256            // Add the estimates that might have been skipped.
257            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        // And reverse to maintain the order of estimates
268        smoothed.reverse();
269
270        Ok(smoothed)
271    }
272
273    /// Returns the root mean square of the prefit residual ratios
274    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    /// Allows iterating on the filter solution. Requires specifying a smoothing condition to know where to stop the smoothing.
283    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            // Prevent infinite loop when iterating prior to turning on the EKF.
308            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            // First, smooth the estimates
317            let smoothed = self.smooth(config.smoother)?;
318            // Reset the propagator
319            self.prop.state = self.init_state;
320            // Empty the estimates and add the first smoothed estimate as the initial estimate
321            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            // And re-run the filter
326            self.process_arc(arc)?;
327
328            // Compute the new RMS
329            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                // Reset the counter
384                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    /// Process the provided measurements for this orbit determination process given the associated devices.
411    ///
412    /// # Argument details
413    /// + The measurements must be a list mapping the name of the measurement device to the measurement itself.
414    /// + The name of all measurement devices must be present in the provided devices, i.e. the key set of `devices` must be a superset of the measurement device names present in the list.
415    /// + The maximum step size to ensure we don't skip any measurements.
416    #[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        // Check proper configuration.
443        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        // Start by propagating the estimator.
455        let num_msrs = measurements.len();
456
457        // Update the step size of the navigation propagator if it isn't already fixed step
458        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; // Prevent showing "0% done"
469        info!("Processing {num_msrs} measurements with covariance mapping");
470
471        // We'll build a trajectory of the estimated states. This will be used to compute the measurements.
472        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            // Advance the propagator
481            loop {
482                let delta_t = next_msr_epoch - epoch;
483
484                // Propagator for the minimum time between the maximum step size, the next step size, and the duration to the next measurement.
485                let next_step_size = delta_t.min(self.prop.step_size).min(max_step);
486
487                // Remove old states from the trajectory
488                // This is a manual implementation of `retaint` because we know it's a sorted vec, so no need to resort every time
489                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                    // NOTE: At the time being, only spacecraft estimation is possible, and the trajectory will always be the exact state
506                    // that was propagated. Even once ground station biases are estimated, these won't go through the propagator.
507                    traj.states.push(state);
508                }
509
510                // Now that we've advanced the propagator, let's see whether we're at the time of the next measurement.
511
512                // Extract the state and update the STM in the filter.
513                let nominal_state = self.prop.state;
514                // Get the datetime and info needed to compute the theoretical measurement according to the model
515                epoch = nominal_state.epoch();
516
517                // Perform a measurement update
518                if nominal_state.epoch() == next_msr_epoch {
519                    // Get the computed observations
520                    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                                // Switch back from extended if necessary
528                                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                                // Perform several measurement updates to ensure the desired dimensionality.
536                                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                                        // We've processed all measurements.
551                                        break;
552                                    }
553
554                                    // Check that the observation is valid.
555                                    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                                                // real_obs = measured_obs + k * modulus
594                                                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                                            // Switch to EKF if necessary, and update the dynamics and such
618                                            // Note: we call enable_ekf first to ensure that the trigger gets
619                                            // called in case it needs to save some information (e.g. the
620                                            // StdEkfTrigger needs to store the time of the previous measurement).
621
622                                            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                    // No measurement can be used here, let's just do a time update and continue advancing the propagator.
680                    debug!("time update {epoch}");
681                    match self.kf.time_update(nominal_state) {
682                        Ok(est) => {
683                            // State deviation is always zero for an EKF time update
684                            // therefore we don't do anything different for an extended filter
685                            self.estimates.push(est);
686                            // We push None so that the residuals and estimates are aligned
687                            self.residuals.push(None);
688                        }
689                        Err(e) => return Err(e),
690                    }
691                    self.prop.state.reset_stm();
692                }
693            }
694        }
695
696        // Always report the 100% mark
697        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    /// Continuously predicts the trajectory until the provided end epoch, with covariance mapping at each step. In other words, this performs a time update.
709    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            // Perform time update
722
723            // Extract the state and update the STM in the filter.
724            let nominal_state = self.prop.state;
725            // Get the datetime and info needed to compute the theoretical measurement according to the model
726            epoch = nominal_state.epoch();
727            // No measurement can be used here, let's just do a time update
728            debug!("time update {epoch}");
729            match self.kf.time_update(nominal_state) {
730                Ok(est) => {
731                    // State deviation is always zero for an EKF time update
732                    // therefore we don't do anything different for an extended filter
733                    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    /// Continuously predicts the trajectory for the provided duration, with covariance mapping at each step. In other words, this performs a time update.
748    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    /// Builds the navigation trajectory for the estimated state only
754    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}