Skip to main content

statime_algo/
filter.rs

1use statime_base::{
2    ClockId, DirectedLinkId, Direction, Duration, LeapStatus, LinkId, TAI, TimeSnapshot, Timestamp,
3};
4
5use crate::{
6    AlgoError,
7    estimator::{EstimatorState, UncertainValue},
8    link_noise::{LinkDelayNoiseEstimate, LinkNoiseEstimator},
9    ringbuffer::UnorderedRingBuffer,
10    storage::{FilterLinkStorage, KalmanStorageBase},
11};
12
13#[cfg(not(feature = "std"))]
14use crate::float_polyfill::FloatPolyfill;
15
16/// Configuration for a measurement link between clocks.
17#[derive(Debug, Clone, Copy, PartialEq)]
18pub struct LinkConfig {
19    /// Error bound used to calculate the desired poll rate for the link.
20    pub desired_error_bound: Duration,
21    /// The period of the measurement, for measurements which occur from repeating
22    /// signals which don't contain a complete timestamp. This is used to then infer
23    /// the missing offset information.
24    pub period: Option<Duration>,
25}
26
27impl Default for LinkConfig {
28    fn default() -> Self {
29        Self {
30            // 10 ms is reachable quite easily even over poor networks, and a reasonable default target.
31            desired_error_bound: Duration::from_seconds_nanos(0, 10_000_000),
32            period: None,
33        }
34    }
35}
36
37/// Additional configuration specific for tracked links.
38#[derive(Debug, Clone, Copy, PartialEq)]
39pub struct TrackedLinkConfig {
40    /// Rate at which we become uncertain of link delay, as fraction of the
41    /// delay per second.
42    pub decay_rate: f64,
43    /// The longest interval allowed between 2 measurements when using them for
44    /// estimating the link delay.
45    pub longest_interval_for_delay_estimation: Duration,
46}
47
48impl Default for TrackedLinkConfig {
49    fn default() -> Self {
50        Self {
51            decay_rate: 0.01,
52            longest_interval_for_delay_estimation: Duration::from_seconds_nanos(0, 500_000_000),
53        }
54    }
55}
56
57#[derive(Debug, Clone)]
58enum LinkState {
59    Tracked {
60        link_noise_estimator: LinkNoiseEstimator,
61        tracked_config: TrackedLinkConfig,
62    },
63    Untracked,
64}
65
66impl LinkState {
67    /// Get the current delay and noise estimate for the link.
68    ///
69    /// # Errors
70    /// Returns an error if the link is tracked but the delay and noise estimate is not yet available.
71    fn delay_and_noise_estimate(&self) -> Result<LinkDelayNoiseEstimate, AlgoError> {
72        match self {
73            LinkState::Tracked {
74                link_noise_estimator,
75                ..
76            } => Ok(link_noise_estimator.delay_and_noise_estimate()?),
77            LinkState::Untracked => Ok(LinkDelayNoiseEstimate {
78                delay: 0.0,
79                noise: 0.0,
80            }),
81        }
82    }
83
84    /// Get the decay rate for the link.
85    fn decay_rate(&self) -> f64 {
86        match self {
87            LinkState::Tracked { tracked_config, .. } => tracked_config.decay_rate,
88            LinkState::Untracked => 0.0,
89        }
90    }
91
92    /// Update estimates based on a new measurement.
93    fn measurement(&mut self, direction: Direction, offset: UncertainValue, time: Timestamp<TAI>) {
94        if let LinkState::Tracked {
95            link_noise_estimator,
96            tracked_config,
97        } = self
98        {
99            *link_noise_estimator = link_noise_estimator.clone().measurement(
100                direction,
101                offset.value,
102                time,
103                tracked_config.longest_interval_for_delay_estimation,
104            );
105        }
106    }
107
108    /// Returns true if this is a tracked link, false if it is untracked.
109    fn is_tracked(&self) -> bool {
110        matches!(self, LinkState::Tracked { .. })
111    }
112}
113
114#[derive(Debug, Clone)]
115struct ExternalLinkState {
116    last_offsets: UnorderedRingBuffer,
117    last_offset_uncertainty: f64,
118    importance: UnorderedRingBuffer,
119    root_delay: f64,
120    leap_status: Option<LeapStatus>,
121    usable: bool,
122}
123
124#[derive(Debug, Clone, Copy, PartialEq)]
125struct OffsetWindow {
126    low: f64,
127    high: f64,
128}
129
130impl OffsetWindow {
131    /// Returns true if this window overlaps the other window
132    pub fn overlaps(self, other: OffsetWindow) -> bool {
133        self.low <= other.high && self.high >= other.low
134    }
135}
136
137#[derive(Debug, Clone)]
138pub struct LinkInfo {
139    id: LinkId,
140    active: bool,
141    link_state: LinkState,
142    desired_poll_interval: Duration,
143    external_link_state: Option<ExternalLinkState>,
144    config: LinkConfig,
145}
146
147impl LinkInfo {
148    /// Get an offset window for the link.
149    ///
150    /// Returns None if the link is not with an external clock, or if the link is not usable.
151    fn offset_window<Storage: KalmanStorageBase>(
152        &self,
153        config: &ControllerConfig,
154        state: &EstimatorState<Storage>,
155    ) -> Option<OffsetWindow> {
156        let external_link_state = self.external_link_state.as_ref()?;
157
158        if external_link_state.last_offsets.as_ref().is_empty() || !external_link_state.usable {
159            return None;
160        }
161
162        let internal_clock = if state.is_internal_clock(self.id.first_clock()) {
163            self.id.first_clock()
164        } else {
165            self.id.second_clock()
166        };
167
168        let internal_offset = state.clock_offset(internal_clock).ok()?.value;
169
170        #[expect(
171            clippy::cast_precision_loss,
172            reason = "self.last_offsets length is always exactly representable as f64"
173        )]
174        let avg_offset = external_link_state
175            .last_offsets
176            .as_ref()
177            .iter()
178            .sum::<f64>()
179            / (external_link_state.last_offsets.as_ref().len() as f64);
180
181        let LinkDelayNoiseEstimate { delay, noise } =
182            self.link_state.delay_and_noise_estimate().ok()?;
183        let half_window_size = noise * config.select_link_uncertainty_window
184            + external_link_state.last_offset_uncertainty * config.select_offset_uncertainty_window
185            + (delay + external_link_state.root_delay) * config.select_delay_uncertainty_window;
186
187        if half_window_size < config.select_max_window_size.as_seconds() / 2.0 {
188            Some(OffsetWindow {
189                low: avg_offset - internal_offset - half_window_size,
190                high: avg_offset - internal_offset + half_window_size,
191            })
192        } else {
193            None
194        }
195    }
196
197    fn update_link_offsets_from_steer(&mut self, steered_clock: ClockId, offset_change: f64) {
198        if let Some(external_state) = &mut self.external_link_state
199            && self.id.contains_clock(steered_clock)
200        {
201            for offset in external_state.last_offsets.as_mut() {
202                *offset += offset_change;
203            }
204        }
205    }
206}
207
208/// A list of links, with some utility functions for finding and iterating over
209/// them.
210#[derive(Debug, Clone)]
211struct LinkInfoList<L>(L);
212
213impl<L: FilterLinkStorage> LinkInfoList<L> {
214    /// Create a new, empty `LinkInfoList`.
215    fn new() -> Self {
216        LinkInfoList(L::new())
217    }
218
219    /// Find a link by its id, returning a reference to it.
220    ///
221    /// # Errors
222    /// Returns an error if the link is not found.
223    fn find_by_id(&self, id: LinkId) -> Result<&LinkInfo, AlgoError> {
224        self.0
225            .iter()
226            .find(|info| info.id == id)
227            .ok_or(AlgoError::UnknownLink(id))
228    }
229
230    /// Find a link by its id, returning a mutable reference to it.
231    ///
232    /// # Errors
233    /// Returns an error if the link is not found.
234    fn find_by_id_mut(&mut self, id: LinkId) -> Result<&mut LinkInfo, AlgoError> {
235        self.0
236            .iter_mut()
237            .find(|info| info.id == id)
238            .ok_or(AlgoError::UnknownLink(id))
239    }
240
241    /// Add a new link to the list.
242    fn add_link(&mut self, link: LinkInfo) {
243        self.0.push(link);
244    }
245
246    /// Remove a link from the list.
247    fn remove_link(&mut self, id: LinkId) -> Result<LinkInfo, AlgoError> {
248        let index = self
249            .0
250            .iter()
251            .position(|info| info.id == id)
252            .ok_or(AlgoError::UnknownLink(id))?;
253        Ok(self.0.remove(index))
254    }
255
256    /// Iterate over the links in the list.
257    fn iter(&self) -> impl Iterator<Item = &LinkInfo> {
258        self.into_iter()
259    }
260}
261
262impl<'a, L: FilterLinkStorage> IntoIterator for &'a LinkInfoList<L> {
263    type Item = &'a LinkInfo;
264    type IntoIter = core::slice::Iter<'a, LinkInfo>;
265
266    fn into_iter(self) -> Self::IntoIter {
267        self.0.iter()
268    }
269}
270
271impl<'a, L: FilterLinkStorage> IntoIterator for &'a mut LinkInfoList<L> {
272    type Item = &'a mut LinkInfo;
273    type IntoIter = core::slice::IterMut<'a, LinkInfo>;
274
275    fn into_iter(self) -> Self::IntoIter {
276        self.0.iter_mut()
277    }
278}
279
280/// A state estimation filter with full support for handling selection an dprocessing of links.
281#[derive(Debug, Clone)]
282pub struct LinkFilter<Storage: KalmanStorageBase> {
283    links: LinkInfoList<Storage::FilterLinkStorage>,
284    estimation_state: EstimatorState<Storage>,
285}
286
287/// Configuration global to the [`KalmanController`](crate::KalmanController).
288///
289/// This contains parameters controlling how individual sources are selected.
290#[derive(Debug, Clone)]
291pub struct ControllerConfig {
292    /// Size of the window of uncertainty to assume around source offset for
293    /// judging whether it is a truechimer, based on the observed noise in the
294    /// offset.
295    pub select_offset_uncertainty_window: f64,
296    /// Size of the window of uncertainty to assume around source offset for
297    /// judging whether it is a truechimer, based on the observed link delay
298    /// noise.
299    pub select_link_uncertainty_window: f64,
300    /// Size of the window of uncertainty to assume around source offset for
301    /// judging whether it is a truechimer, from potential asymmetry due to
302    /// delay and root delay.
303    pub select_delay_uncertainty_window: f64,
304    /// Maximum size of the window of uncertainty before we judge a source to
305    /// be unsuitable for synchronization.
306    pub select_max_window_size: Duration,
307    /// Minimum number of sources that need to agree on the current time before
308    /// we enable synchronization.
309    pub minimum_agreeing_sources: usize,
310}
311
312impl<Storage: KalmanStorageBase> LinkFilter<Storage> {
313    /// Create a new, empty `LinkFilter`.
314    #[must_use]
315    pub fn empty(time: Timestamp<TAI>) -> Self {
316        LinkFilter {
317            links: LinkInfoList::new(),
318            estimation_state: EstimatorState::empty(time),
319        }
320    }
321
322    /// Progress the time of the filter.
323    ///
324    /// # Errors
325    /// Returns an error if the new time is before the current time of the filter.
326    pub fn progress_time(mut self, new_time: Timestamp<TAI>) -> Result<Self, AlgoError> {
327        self.estimation_state = self.estimation_state.progress_time(new_time)?;
328        Ok(self)
329    }
330
331    /// Absorb a frequency change of one of the clocks.
332    ///
333    /// # Errors
334    /// Returns an error if the steered clock is not known to the filter.
335    pub fn absorb_frequency_steer(
336        mut self,
337        steered_clock: ClockId,
338        frequency_change: f64,
339    ) -> Result<Self, AlgoError> {
340        self.estimation_state = self
341            .estimation_state
342            .absorb_frequency_steer(steered_clock, frequency_change)?;
343        Ok(self)
344    }
345
346    /// Absorb a step change in the phase of a clock.
347    ///
348    /// This function assumes steering happens at the current filter time.
349    ///
350    /// # Errors
351    /// Returns an error if the steered clock is unknown to the filter.
352    pub fn absorb_offset_change(
353        mut self,
354        steered_clock: ClockId,
355        offset_change: f64,
356    ) -> Result<Self, AlgoError> {
357        self.estimation_state = self
358            .estimation_state
359            .absorb_offset_change(steered_clock, offset_change)?;
360        for link in &mut self.links {
361            if let LinkState::Tracked {
362                link_noise_estimator,
363                ..
364            } = &mut link.link_state
365            {
366                *link_noise_estimator = link_noise_estimator
367                    .clone()
368                    .absorb_offset_change(steered_clock);
369            }
370
371            link.update_link_offsets_from_steer(steered_clock, offset_change);
372        }
373        Ok(self)
374    }
375
376    /// Absorb a step change in the phase of a clock which is also used for the filter time.
377    ///
378    /// This function assumes steering happens at the current filter time.
379    ///
380    /// # Errors
381    /// Returns an error if the steered clock is unknown to the filter.
382    pub fn absorb_system_clock_offset_change(
383        mut self,
384        steered_clock: ClockId,
385        offset_change: Duration,
386    ) -> Result<Self, AlgoError> {
387        self.estimation_state = self
388            .estimation_state
389            .absorb_system_clock_offset_change(steered_clock, offset_change)?;
390        for link in &mut self.links {
391            if let LinkState::Tracked {
392                link_noise_estimator,
393                ..
394            } = &mut link.link_state
395            {
396                *link_noise_estimator = link_noise_estimator
397                    .clone()
398                    .absorb_system_clock_offset_change(steered_clock, offset_change);
399            }
400
401            link.update_link_offsets_from_steer(steered_clock, offset_change.as_seconds());
402        }
403
404        Ok(self)
405    }
406
407    /// Process a measurement from one of the links.
408    ///
409    /// # Errors
410    /// Returns an error if the provided link is unknown.
411    pub fn measurement(
412        mut self,
413        config: &ControllerConfig,
414        direction: DirectedLinkId,
415        mut offset: UncertainValue,
416        system_clock: ClockId,
417    ) -> Result<Self, AlgoError> {
418        let link = self.links.find_by_id_mut(direction.link_id())?;
419
420        link.link_state.measurement(
421            direction.direction(),
422            offset,
423            self.estimation_state.current_time(),
424        );
425        let Ok(estimates) = link.link_state.delay_and_noise_estimate() else {
426            // Delay and noise not known yet, so link not yet usable, not even for basic offset estimation.
427            return Ok(self);
428        };
429        let is_tracked_link = link.link_state.is_tracked();
430
431        // We deal with periodic links by waiting for our underlying estimate to become sufficiently precise
432        // for us to use it to determine with good certainty which exact period the measurement refers to.
433        //
434        // Once this is the case, we convert the measurement here, and then proceed with the measurement as
435        // if it is just a normal measurement.
436        //
437        // Should the estimation ever become too inaccurate for that purpose, we fall back and deactivate
438        // the link.
439        if let Some(period) = link.config.period {
440            let period = period.as_seconds();
441            let prediction = self
442                .estimation_state
443                .prediction(direction, is_tracked_link)?;
444            if 4.0 * prediction.uncertainty() < period {
445                // We have enough precision in our prediction, collapse to close
446                let delta = (offset.value - prediction.value).rem_euclid(period);
447                if delta > period / 2.0 {
448                    offset.value = delta + prediction.value - period;
449                } else {
450                    offset.value = delta + prediction.value;
451                }
452            } else {
453                self.estimation_state = Self::deactivate_link(link, self.estimation_state)?;
454                // Clear the offset estimate because it is now unreliable
455                if let Some(external_link_state) = &mut link.external_link_state {
456                    external_link_state.last_offsets = UnorderedRingBuffer::default();
457                }
458                return Ok(self);
459            }
460        }
461
462        if let Some(external_link_state) = &mut link.external_link_state {
463            // Ensure offset is calculated relative to the external clock, flipping signs
464            // if the measurement direction was different.
465            let offset_to_external = if self
466                .estimation_state
467                .is_external_clock(direction.from_clock())
468            {
469                offset.value - estimates.delay
470            } else {
471                -(offset.value - estimates.delay)
472            };
473
474            external_link_state.last_offsets.insert(offset_to_external);
475            external_link_state.last_offset_uncertainty = offset.uncertainty();
476
477            let our_window = link.offset_window(config, &self.estimation_state);
478
479            let Some(consensus_window) = self.find_external_consensus_window(config) else {
480                // If there is no consensus currently, wait until there is before we start
481                // discarding links. This ensures that short-term disagreement doesn't
482                // immediately reset synchronization, but that we still don't steer without
483                // consensus.
484                //
485                // We do reset the poll interval to speed up recovery.
486                self.links
487                    .find_by_id_mut(direction.link_id())?
488                    .desired_poll_interval = Duration::ZERO;
489                return Ok(self);
490            };
491
492            let link = self.links.find_by_id_mut(direction.link_id())?;
493
494            // We are active if and only if our window overlaps with the consensus window.
495            if let Some(our_window) = our_window
496                && our_window.overlaps(consensus_window)
497            {
498                self.estimation_state =
499                    Self::activate_link(link, self.estimation_state, estimates)?;
500            } else {
501                self.estimation_state = Self::deactivate_link(link, self.estimation_state)?;
502                return Ok(self);
503            }
504        } else {
505            self.estimation_state = Self::activate_link(link, self.estimation_state, estimates)?;
506        }
507
508        let (new_estimation_state, importance) = self.estimation_state.measurement(
509            direction,
510            offset.add_uncertainty(estimates.noise),
511            is_tracked_link,
512            system_clock,
513        )?;
514
515        self.estimation_state = new_estimation_state;
516
517        let link = self.links.find_by_id_mut(direction.link_id())?;
518        if let Some(external_state) = &mut link.external_link_state {
519            external_state.importance.insert(importance);
520        }
521
522        self.update_desired_poll(direction.link_id())
523    }
524
525    // This is an associated function instead of a method to better deal with lifetimes
526    // in the measurement function
527    fn activate_link(
528        link: &mut LinkInfo,
529        mut estimation_state: EstimatorState<Storage>,
530        estimates: LinkDelayNoiseEstimate,
531    ) -> Result<EstimatorState<Storage>, AlgoError> {
532        if !link.active {
533            link.active = true;
534            if link.link_state.is_tracked() {
535                estimation_state = estimation_state.add_link(
536                    link.id,
537                    (estimates.delay, estimates.noise).into(),
538                    link.link_state.decay_rate(),
539                )?;
540            }
541        }
542        Ok(estimation_state)
543    }
544
545    // This is an associated function instead of a method to better deal with lifetimes
546    // in the measurement function
547    fn deactivate_link(
548        link: &mut LinkInfo,
549        mut estimation_state: EstimatorState<Storage>,
550    ) -> Result<EstimatorState<Storage>, AlgoError> {
551        if link.active {
552            link.active = false;
553            link.desired_poll_interval = Duration::ZERO;
554            if link.link_state.is_tracked() {
555                estimation_state = estimation_state.remove_link(link.id)?;
556            }
557            if let Some(external_state) = &mut link.external_link_state {
558                external_state.importance = UnorderedRingBuffer::default();
559            }
560        }
561        Ok(estimation_state)
562    }
563
564    fn update_desired_poll(mut self, link_id: LinkId) -> Result<Self, AlgoError> {
565        let link = self.links.find_by_id_mut(link_id)?;
566
567        let desire_first_clock = if self
568            .estimation_state
569            .is_internal_clock(link_id.first_clock())
570        {
571            Some(self.estimation_state.time_to_error_bound_exceedance(
572                link_id.first_clock(),
573                link.config.desired_error_bound,
574            )?)
575        } else {
576            None
577        };
578
579        let desire_second_clock = if self
580            .estimation_state
581            .is_internal_clock(link_id.second_clock())
582        {
583            Some(self.estimation_state.time_to_error_bound_exceedance(
584                link_id.second_clock(),
585                link.config.desired_error_bound,
586            )?)
587        } else {
588            None
589        };
590
591        link.desired_poll_interval = match (desire_first_clock, desire_second_clock) {
592            (Some(first), None) => first,
593            (None, Some(second)) => second,
594            (Some(first), Some(second)) => first.min(second),
595            _ => {
596                return Err(AlgoError::BothClocksExternal(
597                    link_id.first_clock(),
598                    link_id.second_clock(),
599                ));
600            }
601        };
602
603        Ok(self)
604    }
605
606    pub fn external_data_update(
607        mut self,
608        link_id: LinkId,
609        root_delay: f64,
610        leap_status: Option<LeapStatus>,
611        usable: bool,
612    ) -> Result<Self, AlgoError> {
613        let link = self.links.find_by_id_mut(link_id)?;
614        if let Some(external_state) = &mut link.external_link_state {
615            external_state.root_delay = root_delay;
616            external_state.leap_status = leap_status;
617            external_state.usable = usable;
618            Ok(self)
619        } else {
620            Err(AlgoError::LinkNotExternal(link_id))
621        }
622    }
623
624    #[must_use]
625    pub fn leap_vote(&self, config: &ControllerConfig) -> Option<LeapStatus> {
626        let consensus_window = self.find_external_consensus_window(config)?;
627
628        let mut count_none = 0;
629        let mut count_59 = 0;
630        let mut count_61 = 0;
631
632        for link in self.links.iter() {
633            if let Some(window) = link.offset_window(config, &self.estimation_state)
634                && window.overlaps(consensus_window)
635                && let Some(state) = &link.external_link_state
636            {
637                match state.leap_status {
638                    Some(LeapStatus::None) => count_none += 1,
639                    Some(LeapStatus::Leap59) => count_59 += 1,
640                    Some(LeapStatus::Leap61) => count_61 += 1,
641                    None => { /* no vote */ }
642                }
643            }
644        }
645
646        let total = count_none + count_59 + count_61;
647
648        if count_none * 2 > total {
649            Some(LeapStatus::None)
650        } else if count_59 * 2 > total {
651            Some(LeapStatus::Leap59)
652        } else if count_61 * 2 > total {
653            Some(LeapStatus::Leap61)
654        } else {
655            None
656        }
657    }
658
659    #[must_use]
660    pub fn local_root_delay(&self, config: &ControllerConfig) -> Option<f64> {
661        let consensus_window = self.find_external_consensus_window(config)?;
662
663        let mut best_delay = f64::MAX;
664        for link in self.links.iter() {
665            if let Some(window) = link.offset_window(config, &self.estimation_state)
666                && window.overlaps(consensus_window)
667                && let Some(state) = &link.external_link_state
668                && let Ok(estimate) = link.link_state.delay_and_noise_estimate()
669            {
670                best_delay = best_delay.min(state.root_delay + estimate.delay);
671            }
672        }
673
674        Some(best_delay)
675    }
676
677    /// Add an internal clock to the filter.
678    ///
679    /// # Errors
680    /// Returns an error if the clock cannot be added due to capacity constraints.
681    pub fn add_clock(
682        mut self,
683        initial_offset: UncertainValue,
684        initial_frequency: UncertainValue,
685        initial_wander: f64,
686    ) -> Result<(Self, ClockId), AlgoError> {
687        let id = ClockId::new();
688        self.estimation_state = self.estimation_state.add_clock(
689            id,
690            initial_offset,
691            initial_frequency,
692            initial_wander,
693        )?;
694        Ok((self, id))
695    }
696
697    /// Remove an internal clock from the filter.
698    ///
699    /// # Errors
700    /// Returns an error if the clock is not known to the filter or if the
701    /// clock is still in use by any links.
702    pub fn remove_clock(mut self, id: ClockId) -> Result<Self, AlgoError> {
703        if let Some(link) = self.links.iter().find(|l| l.id.contains_clock(id)) {
704            return Err(AlgoError::ClockInUse(id, link.id));
705        }
706
707        self.estimation_state = self.estimation_state.remove_clock(id)?;
708        Ok(self)
709    }
710
711    /// Add a link whos delay, and the link noise, is actively measured.
712    ///
713    /// Note that such links are required to have measurements in both directions to be useful.
714    ///
715    /// # Errors
716    /// Returns an error if the provided clocks are invalid, or an invalid combination for the link.
717    pub fn add_tracked_link(
718        mut self,
719        first_clock: ClockId,
720        second_clock: Option<ClockId>,
721        config: LinkConfig,
722        tracked_config: TrackedLinkConfig,
723    ) -> Result<(Self, LinkId), AlgoError> {
724        if self.estimation_state.is_external_clock(first_clock) {
725            return Err(AlgoError::UnexpectedExternalClock(first_clock));
726        }
727        if !self.estimation_state.is_internal_clock(first_clock) {
728            return Err(AlgoError::UnknownClock(first_clock));
729        }
730
731        let (second_clock, is_internal) = if let Some(second_clock) = second_clock {
732            if self.estimation_state.is_external_clock(second_clock) {
733                return Err(AlgoError::UnexpectedExternalClock(second_clock));
734            }
735            if !self.estimation_state.is_internal_clock(second_clock) {
736                return Err(AlgoError::UnknownClock(second_clock));
737            }
738            (second_clock, true)
739        } else {
740            let second_clock = ClockId::new();
741            self.estimation_state = self.estimation_state.add_external_clock(second_clock)?;
742            (second_clock, false)
743        };
744
745        let id =
746            LinkId::new(first_clock, second_clock).ok_or(AlgoError::ClocksEqual(first_clock))?;
747        self.links.add_link(LinkInfo {
748            id,
749            active: false,
750            link_state: LinkState::Tracked {
751                link_noise_estimator: LinkNoiseEstimator::new(id),
752                tracked_config,
753            },
754            desired_poll_interval: Duration::ZERO,
755            external_link_state: if is_internal {
756                None
757            } else {
758                Some(ExternalLinkState {
759                    last_offsets: UnorderedRingBuffer::default(),
760                    last_offset_uncertainty: 0.0,
761                    importance: UnorderedRingBuffer::default(),
762                    root_delay: 0.0,
763                    leap_status: None,
764                    usable: false,
765                })
766            },
767            config,
768        });
769
770        Ok((self, id))
771    }
772
773    /// Add a link where delay and link noise are non-tracked.
774    ///
775    /// # Errors
776    /// Returns an error if the provided clocks are invalid, or an invalid combination for the link.
777    pub fn add_untracked_link(
778        mut self,
779        first_clock: ClockId,
780        second_clock: Option<ClockId>,
781        config: LinkConfig,
782    ) -> Result<(Self, LinkId), AlgoError> {
783        if self.estimation_state.is_external_clock(first_clock) {
784            return Err(AlgoError::UnexpectedExternalClock(first_clock));
785        }
786        if !self.estimation_state.is_internal_clock(first_clock) {
787            return Err(AlgoError::UnknownClock(first_clock));
788        }
789
790        let (second_clock, is_internal) = if let Some(second_clock) = second_clock {
791            if self.estimation_state.is_external_clock(second_clock) {
792                return Err(AlgoError::UnexpectedExternalClock(second_clock));
793            }
794            if !self.estimation_state.is_internal_clock(second_clock) {
795                return Err(AlgoError::UnknownClock(second_clock));
796            }
797            (second_clock, true)
798        } else {
799            let second_clock = ClockId::new();
800            self.estimation_state = self.estimation_state.add_external_clock(second_clock)?;
801            (second_clock, false)
802        };
803
804        let id =
805            LinkId::new(first_clock, second_clock).ok_or(AlgoError::ClocksEqual(first_clock))?;
806        self.links.add_link(LinkInfo {
807            id,
808            active: is_internal,
809            link_state: LinkState::Untracked,
810            desired_poll_interval: Duration::ZERO,
811            external_link_state: if is_internal {
812                None
813            } else {
814                Some(ExternalLinkState {
815                    last_offsets: UnorderedRingBuffer::default(),
816                    last_offset_uncertainty: 0.0,
817                    importance: UnorderedRingBuffer::default(),
818                    root_delay: 0.0,
819                    leap_status: None,
820                    usable: false,
821                })
822            },
823            config,
824        });
825
826        Ok((self, id))
827    }
828
829    /// Remove a link from the filter.
830    ///
831    /// # Errors
832    /// Returns an error if the link does not exist.
833    pub fn remove_link(mut self, id: LinkId) -> Result<Self, AlgoError> {
834        let link = self.links.remove_link(id)?;
835        if link.active && matches!(link.link_state, LinkState::Tracked { .. }) {
836            self.estimation_state = self.estimation_state.remove_link(id)?;
837        }
838
839        if self.estimation_state.is_external_clock(id.second_clock()) {
840            self.estimation_state = self
841                .estimation_state
842                .remove_external_clock(id.second_clock())?;
843        }
844
845        Ok(self)
846    }
847
848    /// Check whether a given link is currently active. Note that this reflects
849    /// only whether the last measurement was used, not whether the link would be
850    /// chosen now when a new measurement matching the old ones arrives.
851    ///
852    /// # Errors
853    /// Returns an error if the link does not exist.
854    pub fn link_active(&self, id: LinkId) -> Result<bool, AlgoError> {
855        let link = self.links.find_by_id(id)?;
856        Ok(link.active)
857    }
858
859    pub fn link_desired_poll_interval(&self, id: LinkId) -> Result<Duration, AlgoError> {
860        let link = self.links.find_by_id(id)?;
861        Ok(link.desired_poll_interval)
862    }
863
864    fn find_external_consensus_window(&self, config: &ControllerConfig) -> Option<OffsetWindow> {
865        let mut bounds: Storage::BoundStorage = self
866            .links
867            .iter()
868            .filter_map(|info| {
869                info.offset_window(config, &self.estimation_state)
870                    .map(|window| {
871                        [
872                            (window.low, BoundType::Start),
873                            (window.high, BoundType::End),
874                        ]
875                    })
876            })
877            .flatten()
878            .collect();
879
880        bounds.sort_unstable_by(|a, b| a.0.total_cmp(&b.0).then_with(|| a.1.cmp(&b.1)));
881
882        // Find the intersection of the confidence intervals of the maximum
883        // overlapping set. We need this entire interval to properly integrate
884        // periodic sources
885        let mut maxlow: usize = 0;
886        let mut maxhigh: usize = 0;
887        let mut max_offset_low: f64 = 0.0;
888        let mut max_offset_high: f64 = 0.0;
889        let mut cur: usize = 0;
890
891        for (offset, boundtype) in &*bounds {
892            match boundtype {
893                BoundType::Start => {
894                    cur += 1;
895                    if cur > maxlow {
896                        maxlow = cur;
897                        max_offset_low = *offset;
898                    }
899                }
900                BoundType::End => {
901                    if cur > maxhigh {
902                        maxhigh = cur;
903                        max_offset_high = *offset;
904                    }
905                    cur -= 1;
906                }
907            }
908        }
909
910        // Check that the lower and upper bound of the intersection agree on how many
911        // sources are part of the maximum set. If not, something has seriously gone
912        // wrong and we shouldn't steer the clock.
913        assert_eq!(maxlow, maxhigh);
914        let max = maxlow;
915
916        if max >= config.minimum_agreeing_sources && max * 4 > bounds.len() {
917            Some(OffsetWindow {
918                low: max_offset_low,
919                high: max_offset_high,
920            })
921        } else {
922            None
923        }
924    }
925
926    /// Get the current offset of a clock.
927    ///
928    /// # Errors
929    /// Fails when the clock is not known to the filter.
930    pub fn clock_offset(&self, clock_id: ClockId) -> Result<UncertainValue, AlgoError> {
931        self.estimation_state.clock_offset(clock_id)
932    }
933
934    /// Get the current frequency offset of a clock.
935    ///
936    /// # Errors
937    /// Fails when the clock is not known to the filter.
938    pub fn clock_frequency(&self, clock_id: ClockId) -> Result<UncertainValue, AlgoError> {
939        self.estimation_state.clock_frequency(clock_id)
940    }
941
942    pub fn clock_snapshot(&self, id: ClockId) -> Result<TimeSnapshot, AlgoError> {
943        self.estimation_state.clock_snapshot(id)
944    }
945
946    #[expect(
947        clippy::cast_precision_loss,
948        reason = "Buffer length always fits in f64 without precision loss."
949    )]
950    pub fn link_importance(&self, id: LinkId) -> Result<Option<f64>, AlgoError> {
951        let link = self.links.find_by_id(id)?;
952        if let Some(external_state) = &link.external_link_state {
953            let buf = external_state.importance.as_ref();
954            if !link.active || buf.is_empty() {
955                Ok(None)
956            } else {
957                Ok(Some(buf.iter().sum::<f64>() / (buf.len() as f64)))
958            }
959        } else {
960            Err(AlgoError::LinkNotExternal(id))
961        }
962    }
963
964    #[cfg(feature = "std")]
965    pub fn active_links(&self) -> std::vec::Vec<statime_base::ActiveLinkData> {
966        self.links
967            .iter()
968            .filter_map(|link| {
969                self.link_importance(link.id)
970                    .ok()
971                    .flatten()
972                    .map(|importance| statime_base::ActiveLinkData {
973                        id: link.id,
974                        importance,
975                    })
976            })
977            .collect()
978    }
979}
980
981#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
982pub enum BoundType {
983    Start,
984    End,
985}
986
987#[cfg(all(test, feature = "std"))]
988#[allow(clippy::too_many_lines, reason = "Test code")]
989mod tests {
990    use statime_base::{ClockId, Duration, LeapStatus, Timestamp};
991
992    use crate::{
993        AlgoError, LinkConfig, StdKalmanStorage, TrackedLinkConfig,
994        estimator::UncertainValue,
995        filter::{ControllerConfig, LinkFilter},
996    };
997
998    #[test]
999    fn untracked_internal_link_is_always_active() {
1000        let config = ControllerConfig {
1001            select_offset_uncertainty_window: 2.0,
1002            select_link_uncertainty_window: 2.0,
1003            select_delay_uncertainty_window: 0.7,
1004            select_max_window_size: Duration::from_seconds_nanos(1, 0),
1005            minimum_agreeing_sources: 3,
1006        };
1007
1008        let filter = LinkFilter::<StdKalmanStorage<()>>::empty(Timestamp::UNIX_EPOCH);
1009        let (filter, clock_a) = filter
1010            .add_clock((0.0, 0.0).into(), (0.0, 0.0).into(), 1e-8)
1011            .unwrap();
1012        let (filter, clock_b) = filter
1013            .add_clock((0.0, 0.0).into(), (0.0, 0.0).into(), 1e-8)
1014            .unwrap();
1015        let (filter, link_id) = filter
1016            .add_untracked_link(clock_a, Some(clock_b), LinkConfig::default())
1017            .unwrap();
1018        assert!(filter.link_active(link_id).unwrap());
1019        assert_eq!(
1020            filter.link_desired_poll_interval(link_id).unwrap(),
1021            Duration::ZERO
1022        );
1023        let filter = filter
1024            .measurement(&config, link_id.forward(), (0.0, 0.01).into(), clock_a)
1025            .unwrap();
1026        assert_ne!(
1027            filter.link_desired_poll_interval(link_id).unwrap(),
1028            Duration::ZERO
1029        );
1030    }
1031
1032    #[test]
1033    fn tracked_internal_link_becomes_active() {
1034        let config = ControllerConfig {
1035            select_offset_uncertainty_window: 2.0,
1036            select_link_uncertainty_window: 2.0,
1037            select_delay_uncertainty_window: 0.7,
1038            select_max_window_size: Duration::from_seconds_nanos(1, 0),
1039            minimum_agreeing_sources: 3,
1040        };
1041
1042        let filter = LinkFilter::<StdKalmanStorage<()>>::empty(Timestamp::UNIX_EPOCH);
1043        let (filter, clock_a) = filter
1044            .add_clock((0.0, 0.0).into(), (0.0, 0.0).into(), 1e-8)
1045            .unwrap();
1046        let (filter, clock_b) = filter
1047            .add_clock((0.0, 0.0).into(), (0.0, 0.0).into(), 1e-8)
1048            .unwrap();
1049        let (filter, link_id) = filter
1050            .add_tracked_link(
1051                clock_a,
1052                Some(clock_b),
1053                LinkConfig::default(),
1054                TrackedLinkConfig::default(),
1055            )
1056            .unwrap();
1057
1058        assert!(!filter.link_active(link_id).unwrap());
1059        assert_eq!(
1060            filter.link_desired_poll_interval(link_id).unwrap(),
1061            Duration::ZERO
1062        );
1063
1064        let filter = filter
1065            .measurement(&config, link_id.forward(), (0.0, 0.001).into(), clock_a)
1066            .unwrap()
1067            .measurement(&config, link_id.reverse(), (0.0, 0.001).into(), clock_a)
1068            .unwrap()
1069            .measurement(&config, link_id.forward(), (0.0, 0.001).into(), clock_a)
1070            .unwrap()
1071            .measurement(&config, link_id.reverse(), (0.0, 0.001).into(), clock_a)
1072            .unwrap()
1073            .measurement(&config, link_id.forward(), (0.0, 0.001).into(), clock_a)
1074            .unwrap()
1075            .measurement(&config, link_id.reverse(), (0.0, 0.001).into(), clock_a)
1076            .unwrap()
1077            .measurement(&config, link_id.forward(), (0.0, 0.001).into(), clock_a)
1078            .unwrap()
1079            .measurement(&config, link_id.reverse(), (0.0, 0.001).into(), clock_a)
1080            .unwrap();
1081
1082        assert!(filter.link_active(link_id).unwrap());
1083        assert_ne!(
1084            filter.link_desired_poll_interval(link_id).unwrap(),
1085            Duration::ZERO
1086        );
1087    }
1088
1089    #[test]
1090    fn external_links_need_consensus() {
1091        let config = ControllerConfig {
1092            select_offset_uncertainty_window: 2.0,
1093            select_link_uncertainty_window: 2.0,
1094            select_delay_uncertainty_window: 0.7,
1095            select_max_window_size: Duration::from_seconds_nanos(1, 0),
1096            minimum_agreeing_sources: 1,
1097        };
1098
1099        let filter = LinkFilter::<StdKalmanStorage<()>>::empty(Timestamp::UNIX_EPOCH);
1100        let (filter, clock_int) = filter
1101            .add_clock((0.0, 0.0).into(), (0.0, 0.0).into(), 1e-8)
1102            .unwrap();
1103        let (filter, link_1) = filter
1104            .add_tracked_link(
1105                clock_int,
1106                None,
1107                LinkConfig::default(),
1108                TrackedLinkConfig::default(),
1109            )
1110            .unwrap();
1111        let (filter, link_2) = filter
1112            .add_tracked_link(
1113                clock_int,
1114                None,
1115                LinkConfig::default(),
1116                TrackedLinkConfig::default(),
1117            )
1118            .unwrap();
1119        let (filter, link_3) = filter
1120            .add_tracked_link(
1121                clock_int,
1122                None,
1123                LinkConfig::default(),
1124                TrackedLinkConfig::default(),
1125            )
1126            .unwrap();
1127
1128        let filter = filter
1129            // link 1
1130            .external_data_update(link_1, 0.1, None, true)
1131            .unwrap()
1132            .measurement(&config, link_1.forward(), (0.0, 0.001).into(), clock_int)
1133            .unwrap()
1134            .measurement(&config, link_1.reverse(), (0.0, 0.001).into(), clock_int)
1135            .unwrap()
1136            .measurement(&config, link_1.forward(), (0.0, 0.001).into(), clock_int)
1137            .unwrap()
1138            .measurement(&config, link_1.reverse(), (0.0, 0.001).into(), clock_int)
1139            .unwrap()
1140            .measurement(&config, link_1.forward(), (0.0, 0.001).into(), clock_int)
1141            .unwrap()
1142            .measurement(&config, link_1.reverse(), (0.0, 0.001).into(), clock_int)
1143            .unwrap()
1144            .measurement(&config, link_1.forward(), (0.0, 0.001).into(), clock_int)
1145            .unwrap()
1146            .measurement(&config, link_1.reverse(), (0.0, 0.001).into(), clock_int)
1147            .unwrap()
1148            // link 2
1149            .external_data_update(link_2, 0.1, None, true)
1150            .unwrap()
1151            .measurement(&config, link_2.forward(), (0.0, 0.001).into(), clock_int)
1152            .unwrap()
1153            .measurement(&config, link_2.reverse(), (0.0, 0.001).into(), clock_int)
1154            .unwrap()
1155            .measurement(&config, link_2.forward(), (0.0, 0.001).into(), clock_int)
1156            .unwrap()
1157            .measurement(&config, link_2.reverse(), (0.0, 0.001).into(), clock_int)
1158            .unwrap()
1159            .measurement(&config, link_2.forward(), (0.0, 0.001).into(), clock_int)
1160            .unwrap()
1161            .measurement(&config, link_2.reverse(), (0.0, 0.001).into(), clock_int)
1162            .unwrap()
1163            .measurement(&config, link_2.forward(), (0.0, 0.001).into(), clock_int)
1164            .unwrap()
1165            .measurement(&config, link_2.reverse(), (0.0, 0.001).into(), clock_int)
1166            .unwrap()
1167            // link 3
1168            .external_data_update(link_3, 0.1, None, true)
1169            .unwrap()
1170            .measurement(&config, link_3.forward(), (0.0, 0.001).into(), clock_int)
1171            .unwrap()
1172            .measurement(&config, link_3.reverse(), (0.0, 0.001).into(), clock_int)
1173            .unwrap()
1174            .measurement(&config, link_3.forward(), (0.0, 0.001).into(), clock_int)
1175            .unwrap()
1176            .measurement(&config, link_3.reverse(), (0.0, 0.001).into(), clock_int)
1177            .unwrap()
1178            .measurement(&config, link_3.forward(), (0.0, 0.001).into(), clock_int)
1179            .unwrap()
1180            .measurement(&config, link_3.reverse(), (0.0, 0.001).into(), clock_int)
1181            .unwrap()
1182            .measurement(&config, link_3.forward(), (0.0, 0.001).into(), clock_int)
1183            .unwrap()
1184            .measurement(&config, link_3.reverse(), (0.0, 0.001).into(), clock_int)
1185            .unwrap();
1186
1187        assert!(filter.link_active(link_1).unwrap());
1188        assert!(filter.link_active(link_2).unwrap());
1189        assert!(filter.link_active(link_3).unwrap());
1190
1191        let filter = LinkFilter::<StdKalmanStorage<()>>::empty(Timestamp::UNIX_EPOCH);
1192        let (filter, clock_int) = filter
1193            .add_clock((0.0, 0.0).into(), (0.0, 0.0).into(), 1e-8)
1194            .unwrap();
1195        let (filter, link_1) = filter
1196            .add_tracked_link(
1197                clock_int,
1198                None,
1199                LinkConfig::default(),
1200                TrackedLinkConfig::default(),
1201            )
1202            .unwrap();
1203        let (filter, link_2) = filter
1204            .add_tracked_link(
1205                clock_int,
1206                None,
1207                LinkConfig::default(),
1208                TrackedLinkConfig::default(),
1209            )
1210            .unwrap();
1211        let (filter, link_3) = filter
1212            .add_tracked_link(
1213                clock_int,
1214                None,
1215                LinkConfig::default(),
1216                TrackedLinkConfig::default(),
1217            )
1218            .unwrap();
1219
1220        let filter = filter
1221            // link 1
1222            .external_data_update(link_1, 0.1, None, true)
1223            .unwrap()
1224            .measurement(&config, link_1.forward(), (0.0, 0.001).into(), clock_int)
1225            .unwrap()
1226            .measurement(&config, link_1.reverse(), (0.0, 0.001).into(), clock_int)
1227            .unwrap()
1228            .measurement(&config, link_1.forward(), (0.0, 0.001).into(), clock_int)
1229            .unwrap()
1230            .measurement(&config, link_1.reverse(), (0.0, 0.001).into(), clock_int)
1231            .unwrap()
1232            .measurement(&config, link_1.forward(), (0.0, 0.001).into(), clock_int)
1233            .unwrap()
1234            .measurement(&config, link_1.reverse(), (0.0, 0.001).into(), clock_int)
1235            .unwrap()
1236            .measurement(&config, link_1.forward(), (0.0, 0.001).into(), clock_int)
1237            .unwrap()
1238            .measurement(&config, link_1.reverse(), (0.0, 0.001).into(), clock_int)
1239            .unwrap()
1240            // link 2
1241            .external_data_update(link_2, 0.1, None, true)
1242            .unwrap()
1243            .measurement(&config, link_2.forward(), (0.0, 0.001).into(), clock_int)
1244            .unwrap()
1245            .measurement(&config, link_2.reverse(), (0.0, 0.001).into(), clock_int)
1246            .unwrap()
1247            .measurement(&config, link_2.forward(), (0.0, 0.001).into(), clock_int)
1248            .unwrap()
1249            .measurement(&config, link_2.reverse(), (0.0, 0.001).into(), clock_int)
1250            .unwrap()
1251            .measurement(&config, link_2.forward(), (0.0, 0.001).into(), clock_int)
1252            .unwrap()
1253            .measurement(&config, link_2.reverse(), (0.0, 0.001).into(), clock_int)
1254            .unwrap()
1255            .measurement(&config, link_2.forward(), (0.0, 0.001).into(), clock_int)
1256            .unwrap()
1257            .measurement(&config, link_2.reverse(), (0.0, 0.001).into(), clock_int)
1258            .unwrap()
1259            // link 3
1260            .external_data_update(link_3, 0.1, None, true)
1261            .unwrap()
1262            .measurement(&config, link_3.forward(), (1.0, 0.001).into(), clock_int)
1263            .unwrap()
1264            .measurement(&config, link_3.reverse(), (-1.0, 0.001).into(), clock_int)
1265            .unwrap()
1266            .measurement(&config, link_3.forward(), (1.0, 0.001).into(), clock_int)
1267            .unwrap()
1268            .measurement(&config, link_3.reverse(), (-1.0, 0.001).into(), clock_int)
1269            .unwrap()
1270            .measurement(&config, link_3.forward(), (1.0, 0.001).into(), clock_int)
1271            .unwrap()
1272            .measurement(&config, link_3.reverse(), (-1.0, 0.001).into(), clock_int)
1273            .unwrap()
1274            .measurement(&config, link_3.forward(), (1.0, 0.001).into(), clock_int)
1275            .unwrap()
1276            .measurement(&config, link_3.reverse(), (-1.0, 0.001).into(), clock_int)
1277            .unwrap();
1278
1279        assert!(filter.link_active(link_1).unwrap());
1280        assert!(filter.link_active(link_2).unwrap());
1281        assert!(!filter.link_active(link_3).unwrap());
1282
1283        let filter = LinkFilter::<StdKalmanStorage<()>>::empty(Timestamp::UNIX_EPOCH);
1284        let (filter, clock_int) = filter
1285            .add_clock((0.0, 0.0).into(), (0.0, 0.0).into(), 1e-8)
1286            .unwrap();
1287        let (filter, link_1) = filter
1288            .add_tracked_link(
1289                clock_int,
1290                None,
1291                LinkConfig::default(),
1292                TrackedLinkConfig::default(),
1293            )
1294            .unwrap();
1295        let (filter, link_2) = filter
1296            .add_tracked_link(
1297                clock_int,
1298                None,
1299                LinkConfig::default(),
1300                TrackedLinkConfig::default(),
1301            )
1302            .unwrap();
1303        let (filter, link_3) = filter
1304            .add_tracked_link(
1305                clock_int,
1306                None,
1307                LinkConfig::default(),
1308                TrackedLinkConfig::default(),
1309            )
1310            .unwrap();
1311
1312        let filter = filter
1313            // link 1
1314            .external_data_update(link_1, 0.1, None, true)
1315            .unwrap()
1316            .measurement(&config, link_1.forward(), (0.0, 0.001).into(), clock_int)
1317            .unwrap()
1318            .measurement(&config, link_1.reverse(), (0.0, 0.001).into(), clock_int)
1319            .unwrap()
1320            .measurement(&config, link_1.forward(), (0.0, 0.001).into(), clock_int)
1321            .unwrap()
1322            .measurement(&config, link_1.reverse(), (0.0, 0.001).into(), clock_int)
1323            .unwrap()
1324            .measurement(&config, link_1.forward(), (0.0, 0.001).into(), clock_int)
1325            .unwrap()
1326            .measurement(&config, link_1.reverse(), (0.0, 0.001).into(), clock_int)
1327            .unwrap()
1328            .measurement(&config, link_1.forward(), (0.0, 0.001).into(), clock_int)
1329            .unwrap()
1330            .measurement(&config, link_1.reverse(), (0.0, 0.001).into(), clock_int)
1331            .unwrap()
1332            // link 2
1333            .external_data_update(link_2, 0.1, None, true)
1334            .unwrap()
1335            .measurement(&config, link_2.forward(), (1.0, 0.001).into(), clock_int)
1336            .unwrap()
1337            .measurement(&config, link_2.reverse(), (-1.0, 0.001).into(), clock_int)
1338            .unwrap()
1339            .measurement(&config, link_2.forward(), (1.0, 0.001).into(), clock_int)
1340            .unwrap()
1341            .measurement(&config, link_2.reverse(), (-1.0, 0.001).into(), clock_int)
1342            .unwrap()
1343            .measurement(&config, link_2.forward(), (1.0, 0.001).into(), clock_int)
1344            .unwrap()
1345            .measurement(&config, link_2.reverse(), (-1.0, 0.001).into(), clock_int)
1346            .unwrap()
1347            .measurement(&config, link_2.forward(), (1.0, 0.001).into(), clock_int)
1348            .unwrap()
1349            .measurement(&config, link_2.reverse(), (-1.0, 0.001).into(), clock_int)
1350            .unwrap()
1351            // link 3
1352            .external_data_update(link_3, 0.1, None, true)
1353            .unwrap()
1354            .measurement(&config, link_3.forward(), (1.0, 0.001).into(), clock_int)
1355            .unwrap()
1356            .measurement(&config, link_3.reverse(), (-1.0, 0.001).into(), clock_int)
1357            .unwrap()
1358            .measurement(&config, link_3.forward(), (1.0, 0.001).into(), clock_int)
1359            .unwrap()
1360            .measurement(&config, link_3.reverse(), (-1.0, 0.001).into(), clock_int)
1361            .unwrap()
1362            .measurement(&config, link_3.forward(), (1.0, 0.001).into(), clock_int)
1363            .unwrap()
1364            .measurement(&config, link_3.reverse(), (-1.0, 0.001).into(), clock_int)
1365            .unwrap()
1366            .measurement(&config, link_3.forward(), (1.0, 0.001).into(), clock_int)
1367            .unwrap()
1368            .measurement(&config, link_3.reverse(), (-1.0, 0.001).into(), clock_int)
1369            .unwrap();
1370
1371        // Link 1 is still active since it came online first.
1372        assert!(filter.link_active(link_1).unwrap());
1373        // Link 2 is still inactive since, although it is part of the selected set,
1374        // it hasn't yet contributed.
1375        assert!(!filter.link_active(link_2).unwrap());
1376        // Link 3 caused the change so reflects the activity status.
1377        assert!(filter.link_active(link_3).unwrap());
1378
1379        let filter = filter
1380            .measurement(&config, link_1.forward(), (0.0, 0.001).into(), clock_int)
1381            .unwrap()
1382            .measurement(&config, link_2.forward(), (1.0, 0.001).into(), clock_int)
1383            .unwrap();
1384        //
1385        assert!(!filter.link_active(link_1).unwrap());
1386        assert!(filter.link_active(link_2).unwrap());
1387    }
1388
1389    #[test]
1390    fn external_links_inactive_when_unusable() {
1391        let config = ControllerConfig {
1392            select_offset_uncertainty_window: 2.0,
1393            select_link_uncertainty_window: 2.0,
1394            select_delay_uncertainty_window: 0.7,
1395            select_max_window_size: Duration::from_seconds_nanos(1, 0),
1396            minimum_agreeing_sources: 1,
1397        };
1398
1399        let filter = LinkFilter::<StdKalmanStorage<()>>::empty(Timestamp::UNIX_EPOCH);
1400        let (filter, clock_int) = filter
1401            .add_clock((0.0, 0.0).into(), (0.0, 0.0).into(), 1e-8)
1402            .unwrap();
1403        let (filter, link_1) = filter
1404            .add_untracked_link(clock_int, None, LinkConfig::default())
1405            .unwrap();
1406        let filter = filter
1407            .external_data_update(link_1, 0.1, None, true)
1408            .unwrap()
1409            .measurement(&config, link_1.forward(), (0.0, 1e-4).into(), clock_int)
1410            .unwrap();
1411
1412        assert!(filter.link_active(link_1).unwrap());
1413
1414        let filter = LinkFilter::<StdKalmanStorage<()>>::empty(Timestamp::UNIX_EPOCH);
1415        let (filter, clock_int) = filter
1416            .add_clock((0.0, 0.0).into(), (0.0, 0.0).into(), 1e-8)
1417            .unwrap();
1418        let (filter, link_1) = filter
1419            .add_untracked_link(clock_int, None, LinkConfig::default())
1420            .unwrap();
1421        let filter = filter
1422            .external_data_update(link_1, 0.1, None, false)
1423            .unwrap()
1424            .measurement(&config, link_1.forward(), (0.0, 1e-4).into(), clock_int)
1425            .unwrap();
1426
1427        assert!(!filter.link_active(link_1).unwrap());
1428    }
1429
1430    #[test]
1431    fn internal_link_inactive_on_unusable_measurements() {
1432        let config = ControllerConfig {
1433            select_offset_uncertainty_window: 2.0,
1434            select_link_uncertainty_window: 2.0,
1435            select_delay_uncertainty_window: 0.7,
1436            select_max_window_size: Duration::from_seconds_nanos(1, 0),
1437            minimum_agreeing_sources: 3,
1438        };
1439
1440        let filter = LinkFilter::<StdKalmanStorage<()>>::empty(Timestamp::UNIX_EPOCH);
1441        let (filter, clock_a) = filter
1442            .add_clock((0.0, 0.0).into(), (0.0, 0.0).into(), 1e-8)
1443            .unwrap();
1444        let (filter, clock_b) = filter
1445            .add_clock((0.0, 0.0).into(), (0.0, 0.0).into(), 1e-8)
1446            .unwrap();
1447        let (filter, link_id) = filter
1448            .add_tracked_link(
1449                clock_a,
1450                Some(clock_b),
1451                LinkConfig::default(),
1452                TrackedLinkConfig::default(),
1453            )
1454            .unwrap();
1455
1456        assert!(!filter.link_active(link_id).unwrap());
1457
1458        let filter = filter
1459            .measurement(&config, link_id.forward(), (0.0, 0.0).into(), clock_a)
1460            .unwrap()
1461            .progress_time(Timestamp::UNIX_EPOCH + Duration::from_seconds_nanos(0, 600_000_000))
1462            .unwrap()
1463            .measurement(&config, link_id.reverse(), (0.0, 0.0).into(), clock_a)
1464            .unwrap()
1465            .progress_time(Timestamp::UNIX_EPOCH + Duration::from_seconds_nanos(10, 0))
1466            .unwrap()
1467            .measurement(&config, link_id.forward(), (0.0, 0.0).into(), clock_a)
1468            .unwrap()
1469            .progress_time(Timestamp::UNIX_EPOCH + Duration::from_seconds_nanos(10, 600_000_000))
1470            .unwrap()
1471            .measurement(&config, link_id.reverse(), (0.0, 0.0).into(), clock_a)
1472            .unwrap()
1473            .progress_time(Timestamp::UNIX_EPOCH + Duration::from_seconds_nanos(20, 0))
1474            .unwrap()
1475            .measurement(&config, link_id.forward(), (0.0, 0.0).into(), clock_a)
1476            .unwrap()
1477            .progress_time(Timestamp::UNIX_EPOCH + Duration::from_seconds_nanos(20, 600_000_000))
1478            .unwrap()
1479            .measurement(&config, link_id.reverse(), (0.0, 0.0).into(), clock_a)
1480            .unwrap()
1481            .progress_time(Timestamp::UNIX_EPOCH + Duration::from_seconds_nanos(30, 0))
1482            .unwrap()
1483            .measurement(&config, link_id.forward(), (0.0, 0.0).into(), clock_a)
1484            .unwrap()
1485            .progress_time(Timestamp::UNIX_EPOCH + Duration::from_seconds_nanos(30, 600_000_000))
1486            .unwrap()
1487            .measurement(&config, link_id.reverse(), (0.0, 0.0).into(), clock_a)
1488            .unwrap();
1489
1490        assert!(!filter.link_active(link_id).unwrap());
1491    }
1492
1493    #[test]
1494    fn external_links_activity_and_steering_works() {
1495        let config = ControllerConfig {
1496            select_offset_uncertainty_window: 2.0,
1497            select_link_uncertainty_window: 2.0,
1498            select_delay_uncertainty_window: 0.7,
1499            select_max_window_size: Duration::from_seconds_nanos(1, 0),
1500            minimum_agreeing_sources: 1,
1501        };
1502
1503        let filter = LinkFilter::<StdKalmanStorage<()>>::empty(Timestamp::UNIX_EPOCH);
1504        let (filter, clock_int_1) = filter
1505            .add_clock((0.0, 0.0).into(), (0.0, 0.0).into(), 1e-8)
1506            .unwrap();
1507        let (filter, clock_int_2) = filter
1508            .add_clock((10.0, 0.0).into(), (0.0, 0.0).into(), 1e-8)
1509            .unwrap();
1510        let (filter, link_1) = filter
1511            .add_untracked_link(clock_int_1, None, LinkConfig::default())
1512            .unwrap();
1513        let (filter, link_2) = filter
1514            .add_untracked_link(clock_int_1, None, LinkConfig::default())
1515            .unwrap();
1516        let (filter, link_3) = filter
1517            .add_untracked_link(clock_int_2, None, LinkConfig::default())
1518            .unwrap();
1519
1520        let filter = filter
1521            .external_data_update(link_1, 0.1, None, true)
1522            .unwrap()
1523            .measurement(&config, link_1.forward(), (0.0, 0.001).into(), clock_int_1)
1524            .unwrap()
1525            .external_data_update(link_2, 0.1, None, true)
1526            .unwrap()
1527            .measurement(&config, link_2.forward(), (0.0, 0.001).into(), clock_int_1)
1528            .unwrap()
1529            .external_data_update(link_3, 0.1, None, true)
1530            .unwrap()
1531            .measurement(
1532                &config,
1533                link_3.forward(),
1534                (-10.0, 0.001).into(),
1535                clock_int_1,
1536            )
1537            .unwrap();
1538
1539        assert!(filter.link_active(link_1).unwrap());
1540        assert!(filter.link_active(link_2).unwrap());
1541        assert!(filter.link_active(link_3).unwrap());
1542
1543        let filter = filter
1544            .absorb_offset_change(clock_int_2, -10.0)
1545            .unwrap()
1546            .measurement(&config, link_3.forward(), (0.0, 0.001).into(), clock_int_1)
1547            .unwrap();
1548
1549        assert!(filter.link_active(link_3).unwrap());
1550
1551        let filter = LinkFilter::<StdKalmanStorage<()>>::empty(
1552            Timestamp::UNIX_EPOCH + Duration::from_seconds_nanos(10, 0),
1553        );
1554        let (filter, clock_int_1) = filter
1555            .add_clock((0.0, 0.0).into(), (0.0, 0.0).into(), 1e-8)
1556            .unwrap();
1557        let (filter, clock_int_2) = filter
1558            .add_clock((-10.0, 0.0).into(), (0.0, 0.0).into(), 1e-8)
1559            .unwrap();
1560        let (filter, link_1) = filter
1561            .add_untracked_link(clock_int_1, None, LinkConfig::default())
1562            .unwrap();
1563        let (filter, link_2) = filter
1564            .add_untracked_link(clock_int_1, None, LinkConfig::default())
1565            .unwrap();
1566        let (filter, link_3) = filter
1567            .add_untracked_link(clock_int_2, None, LinkConfig::default())
1568            .unwrap();
1569
1570        let filter = filter
1571            .external_data_update(link_1, 0.1, None, true)
1572            .unwrap()
1573            .measurement(&config, link_1.forward(), (0.0, 0.001).into(), clock_int_1)
1574            .unwrap()
1575            .external_data_update(link_2, 0.1, None, true)
1576            .unwrap()
1577            .measurement(&config, link_2.forward(), (0.0, 0.001).into(), clock_int_1)
1578            .unwrap()
1579            .external_data_update(link_3, 0.1, None, true)
1580            .unwrap()
1581            .measurement(&config, link_3.forward(), (10.0, 0.001).into(), clock_int_1)
1582            .unwrap();
1583
1584        assert!(filter.link_active(link_1).unwrap());
1585        assert!(filter.link_active(link_2).unwrap());
1586        assert!(filter.link_active(link_3).unwrap());
1587
1588        let filter = filter
1589            .absorb_system_clock_offset_change(clock_int_2, Duration::from_seconds_nanos(10, 0))
1590            .unwrap()
1591            .measurement(&config, link_3.forward(), (0.0, 0.001).into(), clock_int_1)
1592            .unwrap();
1593
1594        assert!(filter.link_active(link_3).unwrap());
1595    }
1596
1597    #[test]
1598    fn too_uncertain_external_links_inactive() {
1599        let config = ControllerConfig {
1600            select_offset_uncertainty_window: 2.0,
1601            select_link_uncertainty_window: 2.0,
1602            select_delay_uncertainty_window: 0.7,
1603            select_max_window_size: Duration::from_seconds_nanos(1, 0),
1604            minimum_agreeing_sources: 1,
1605        };
1606
1607        let filter = LinkFilter::<StdKalmanStorage<()>>::empty(Timestamp::UNIX_EPOCH);
1608        let (filter, clock_int) = filter
1609            .add_clock((0.0, 0.0).into(), (0.0, 0.0).into(), 1e-8)
1610            .unwrap();
1611        let (filter, link_1) = filter
1612            .add_untracked_link(clock_int, None, LinkConfig::default())
1613            .unwrap();
1614        let filter = filter
1615            .external_data_update(link_1, 10.0, None, true)
1616            .unwrap()
1617            .measurement(&config, link_1.forward(), (0.0, 0.0).into(), clock_int)
1618            .unwrap();
1619
1620        assert!(!filter.link_active(link_1).unwrap());
1621
1622        let filter = LinkFilter::<StdKalmanStorage<()>>::empty(Timestamp::UNIX_EPOCH);
1623        let (filter, clock_int) = filter
1624            .add_clock((0.0, 0.0).into(), (0.0, 0.0).into(), 1e-8)
1625            .unwrap();
1626        let (filter, link_1) = filter
1627            .add_untracked_link(clock_int, None, LinkConfig::default())
1628            .unwrap();
1629        let filter = filter
1630            .external_data_update(link_1, 0.0, None, true)
1631            .unwrap()
1632            .measurement(&config, link_1.forward(), (0.0, 1.0).into(), clock_int)
1633            .unwrap();
1634
1635        assert!(!filter.link_active(link_1).unwrap());
1636
1637        let filter = LinkFilter::<StdKalmanStorage<()>>::empty(Timestamp::UNIX_EPOCH);
1638        let (filter, clock_int) = filter
1639            .add_clock((0.0, 0.0).into(), (0.0, 0.0).into(), 1e-8)
1640            .unwrap();
1641        let (filter, link_1) = filter
1642            .add_tracked_link(
1643                clock_int,
1644                None,
1645                LinkConfig::default(),
1646                TrackedLinkConfig {
1647                    decay_rate: 0.0,
1648                    ..TrackedLinkConfig::default()
1649                },
1650            )
1651            .unwrap();
1652        let filter = filter
1653            .external_data_update(link_1, 10.0, None, true)
1654            .unwrap()
1655            .measurement(&config, link_1.forward(), (0.0, 0.0).into(), clock_int)
1656            .unwrap()
1657            .measurement(&config, link_1.reverse(), (0.0, 0.0).into(), clock_int)
1658            .unwrap()
1659            .measurement(&config, link_1.forward(), (1.0, 0.0).into(), clock_int)
1660            .unwrap()
1661            .measurement(&config, link_1.reverse(), (1.0, 0.0).into(), clock_int)
1662            .unwrap()
1663            .measurement(&config, link_1.forward(), (1.0, 0.0).into(), clock_int)
1664            .unwrap()
1665            .measurement(&config, link_1.reverse(), (1.0, 0.0).into(), clock_int)
1666            .unwrap()
1667            .measurement(&config, link_1.forward(), (-1.0, 0.0).into(), clock_int)
1668            .unwrap()
1669            .measurement(&config, link_1.reverse(), (-1.0, 0.0).into(), clock_int)
1670            .unwrap()
1671            .measurement(&config, link_1.forward(), (-1.0, 0.0).into(), clock_int)
1672            .unwrap()
1673            .measurement(&config, link_1.reverse(), (-1.0, 0.0).into(), clock_int)
1674            .unwrap();
1675
1676        assert!(!filter.link_active(link_1).unwrap());
1677    }
1678
1679    #[test]
1680    fn periodic_internal_link() {
1681        let config = ControllerConfig {
1682            select_offset_uncertainty_window: 2.0,
1683            select_link_uncertainty_window: 2.0,
1684            select_delay_uncertainty_window: 0.7,
1685            select_max_window_size: Duration::from_seconds_nanos(1, 0),
1686            minimum_agreeing_sources: 1,
1687        };
1688
1689        let filter = LinkFilter::<StdKalmanStorage<()>>::empty(Timestamp::UNIX_EPOCH);
1690        let (filter, clock_1) = filter
1691            .add_clock((0.0, 0.0).into(), (0.0, 0.0).into(), 1e-8)
1692            .unwrap();
1693        let (filter, clock_2) = filter
1694            .add_clock((0.0, 1e18).into(), (0.0, 0.0).into(), 1e-8)
1695            .unwrap();
1696        let (filter, link_1) = filter
1697            .add_untracked_link(clock_1, Some(clock_2), LinkConfig::default())
1698            .unwrap();
1699        let (filter, link_2) = filter
1700            .add_untracked_link(
1701                clock_1,
1702                Some(clock_2),
1703                LinkConfig {
1704                    period: Some(Duration::from_seconds_nanos(1, 0)),
1705                    ..LinkConfig::default()
1706                },
1707            )
1708            .unwrap();
1709
1710        let filter = filter
1711            .measurement(&config, link_2.forward(), (3.2, 0.0001).into(), clock_1)
1712            .unwrap();
1713        assert_eq!(filter.clock_offset(clock_2).unwrap(), (0.0, 1e18).into());
1714        assert!(!filter.link_active(link_2).unwrap());
1715        let filter = filter
1716            .measurement(&config, link_1.forward(), (5.0, 0.15).into(), clock_1)
1717            .unwrap();
1718        assert!(filter.link_active(link_1).unwrap());
1719        assert_uv_almost_eq!(
1720            filter.clock_offset(clock_2).unwrap(),
1721            UncertainValue::from((5.0, 0.15))
1722        );
1723        let filter = filter
1724            .measurement(&config, link_2.forward(), (3.2, 0.0001).into(), clock_1)
1725            .unwrap();
1726        assert!(filter.link_active(link_2).unwrap());
1727        assert_uv_almost_eq!(
1728            filter.clock_offset(clock_2).unwrap(),
1729            UncertainValue::from((5.2, 0.0001))
1730        );
1731    }
1732
1733    #[test]
1734    fn periodic_external_link() {
1735        let config = ControllerConfig {
1736            select_offset_uncertainty_window: 2.0,
1737            select_link_uncertainty_window: 2.0,
1738            select_delay_uncertainty_window: 0.7,
1739            select_max_window_size: Duration::from_seconds_nanos(1, 0),
1740            minimum_agreeing_sources: 1,
1741        };
1742
1743        let filter = LinkFilter::<StdKalmanStorage<()>>::empty(Timestamp::UNIX_EPOCH);
1744        let (filter, clock_1) = filter
1745            .add_clock((0.0, 1e18).into(), (0.0, 0.0).into(), 1e-8)
1746            .unwrap();
1747        let (filter, link_1) = filter
1748            .add_untracked_link(clock_1, None, LinkConfig::default())
1749            .unwrap();
1750        let (filter, link_2) = filter
1751            .add_untracked_link(
1752                clock_1,
1753                None,
1754                LinkConfig {
1755                    period: Some(Duration::from_seconds_nanos(1, 0)),
1756                    ..LinkConfig::default()
1757                },
1758            )
1759            .unwrap();
1760
1761        let filter = filter
1762            .external_data_update(link_1, 0.0, None, true)
1763            .unwrap()
1764            .external_data_update(link_2, 0.0, None, true)
1765            .unwrap()
1766            .measurement(&config, link_2.reverse(), (3.2, 0.0001).into(), clock_1)
1767            .unwrap();
1768        assert_eq!(filter.clock_offset(clock_1).unwrap(), (0.0, 1e18).into());
1769        assert!(!filter.link_active(link_2).unwrap());
1770        let filter = filter
1771            .measurement(&config, link_1.reverse(), (5.0, 0.15).into(), clock_1)
1772            .unwrap();
1773        assert!(filter.link_active(link_1).unwrap());
1774        assert_uv_almost_eq!(
1775            filter.clock_offset(clock_1).unwrap(),
1776            UncertainValue::from((5.0, 0.15))
1777        );
1778        let filter = filter
1779            .measurement(&config, link_2.reverse(), (3.2, 0.0001).into(), clock_1)
1780            .unwrap();
1781        assert!(filter.link_active(link_2).unwrap());
1782        assert_uv_almost_eq!(
1783            filter.clock_offset(clock_1).unwrap(),
1784            UncertainValue::from((5.2, 0.0001))
1785        );
1786    }
1787
1788    #[test]
1789    fn periodic_link_too_uncertain() {
1790        let config = ControllerConfig {
1791            select_offset_uncertainty_window: 2.0,
1792            select_link_uncertainty_window: 2.0,
1793            select_delay_uncertainty_window: 0.7,
1794            select_max_window_size: Duration::from_seconds_nanos(1, 0),
1795            minimum_agreeing_sources: 1,
1796        };
1797
1798        let filter = LinkFilter::<StdKalmanStorage<()>>::empty(Timestamp::UNIX_EPOCH);
1799        let (filter, clock_1) = filter
1800            .add_clock((0.0, 0.0).into(), (0.0, 0.0).into(), 1e-8)
1801            .unwrap();
1802        let (filter, clock_2) = filter
1803            .add_clock((0.0, 1e18).into(), (0.0, 0.0).into(), 1e-8)
1804            .unwrap();
1805        let (filter, link_1) = filter
1806            .add_untracked_link(clock_1, Some(clock_2), LinkConfig::default())
1807            .unwrap();
1808        let (filter, link_2) = filter
1809            .add_untracked_link(
1810                clock_1,
1811                Some(clock_2),
1812                LinkConfig {
1813                    period: Some(Duration::from_seconds_nanos(1, 0)),
1814                    ..LinkConfig::default()
1815                },
1816            )
1817            .unwrap();
1818
1819        let filter = filter
1820            .measurement(&config, link_2.forward(), (3.2, 0.0001).into(), clock_1)
1821            .unwrap();
1822        assert_eq!(filter.clock_offset(clock_2).unwrap(), (0.0, 1e18).into());
1823        assert!(!filter.link_active(link_2).unwrap());
1824        let filter = filter
1825            .measurement(&config, link_1.forward(), (5.0, 0.3).into(), clock_1)
1826            .unwrap();
1827        assert!(filter.link_active(link_1).unwrap());
1828        assert_uv_almost_eq!(
1829            filter.clock_offset(clock_2).unwrap(),
1830            UncertainValue::from((5.0, 0.3))
1831        );
1832        let filter = filter
1833            .measurement(&config, link_2.forward(), (3.2, 0.0001).into(), clock_1)
1834            .unwrap();
1835        assert!(!filter.link_active(link_2).unwrap());
1836        assert_uv_almost_eq!(
1837            filter.clock_offset(clock_2).unwrap(),
1838            UncertainValue::from((5.0, 0.3))
1839        );
1840    }
1841
1842    #[test]
1843    fn leap_vote() {
1844        let config = ControllerConfig {
1845            select_offset_uncertainty_window: 2.0,
1846            select_link_uncertainty_window: 2.0,
1847            select_delay_uncertainty_window: 0.7,
1848            select_max_window_size: Duration::from_seconds_nanos(1, 0),
1849            minimum_agreeing_sources: 1,
1850        };
1851
1852        let filter = LinkFilter::<StdKalmanStorage<()>>::empty(Timestamp::UNIX_EPOCH);
1853        let (filter, clock_int) = filter
1854            .add_clock((0.0, 0.0).into(), (0.0, 0.0).into(), 1e-8)
1855            .unwrap();
1856        let (filter, link_1) = filter
1857            .add_tracked_link(
1858                clock_int,
1859                None,
1860                LinkConfig::default(),
1861                TrackedLinkConfig::default(),
1862            )
1863            .unwrap();
1864        let (filter, link_2) = filter
1865            .add_tracked_link(
1866                clock_int,
1867                None,
1868                LinkConfig::default(),
1869                TrackedLinkConfig::default(),
1870            )
1871            .unwrap();
1872        let (filter, link_3) = filter
1873            .add_tracked_link(
1874                clock_int,
1875                None,
1876                LinkConfig::default(),
1877                TrackedLinkConfig::default(),
1878            )
1879            .unwrap();
1880
1881        let base_filter = filter
1882            // link 1
1883            .external_data_update(link_1, 0.1, None, true)
1884            .unwrap()
1885            .measurement(&config, link_1.forward(), (0.0, 0.001).into(), clock_int)
1886            .unwrap()
1887            .measurement(&config, link_1.reverse(), (0.0, 0.001).into(), clock_int)
1888            .unwrap()
1889            .measurement(&config, link_1.forward(), (0.0, 0.001).into(), clock_int)
1890            .unwrap()
1891            .measurement(&config, link_1.reverse(), (0.0, 0.001).into(), clock_int)
1892            .unwrap()
1893            .measurement(&config, link_1.forward(), (0.0, 0.001).into(), clock_int)
1894            .unwrap()
1895            .measurement(&config, link_1.reverse(), (0.0, 0.001).into(), clock_int)
1896            .unwrap()
1897            .measurement(&config, link_1.forward(), (0.0, 0.001).into(), clock_int)
1898            .unwrap()
1899            .measurement(&config, link_1.reverse(), (0.0, 0.001).into(), clock_int)
1900            .unwrap()
1901            // link 2
1902            .external_data_update(link_2, 0.1, None, true)
1903            .unwrap()
1904            .measurement(&config, link_2.forward(), (0.0, 0.001).into(), clock_int)
1905            .unwrap()
1906            .measurement(&config, link_2.reverse(), (0.0, 0.001).into(), clock_int)
1907            .unwrap()
1908            .measurement(&config, link_2.forward(), (0.0, 0.001).into(), clock_int)
1909            .unwrap()
1910            .measurement(&config, link_2.reverse(), (0.0, 0.001).into(), clock_int)
1911            .unwrap()
1912            .measurement(&config, link_2.forward(), (0.0, 0.001).into(), clock_int)
1913            .unwrap()
1914            .measurement(&config, link_2.reverse(), (0.0, 0.001).into(), clock_int)
1915            .unwrap()
1916            .measurement(&config, link_2.forward(), (0.0, 0.001).into(), clock_int)
1917            .unwrap()
1918            .measurement(&config, link_2.reverse(), (0.0, 0.001).into(), clock_int)
1919            .unwrap()
1920            // link 3
1921            .external_data_update(link_3, 0.1, None, true)
1922            .unwrap()
1923            .measurement(&config, link_3.forward(), (0.0, 0.001).into(), clock_int)
1924            .unwrap()
1925            .measurement(&config, link_3.reverse(), (0.0, 0.001).into(), clock_int)
1926            .unwrap()
1927            .measurement(&config, link_3.forward(), (0.0, 0.001).into(), clock_int)
1928            .unwrap()
1929            .measurement(&config, link_3.reverse(), (0.0, 0.001).into(), clock_int)
1930            .unwrap()
1931            .measurement(&config, link_3.forward(), (0.0, 0.001).into(), clock_int)
1932            .unwrap()
1933            .measurement(&config, link_3.reverse(), (0.0, 0.001).into(), clock_int)
1934            .unwrap()
1935            .measurement(&config, link_3.forward(), (0.0, 0.001).into(), clock_int)
1936            .unwrap()
1937            .measurement(&config, link_3.reverse(), (0.0, 0.001).into(), clock_int)
1938            .unwrap();
1939
1940        let filter = base_filter
1941            .clone()
1942            .external_data_update(link_1, 0.1, Some(LeapStatus::None), true)
1943            .unwrap()
1944            .external_data_update(link_2, 0.1, Some(LeapStatus::None), true)
1945            .unwrap()
1946            .external_data_update(link_3, 0.1, Some(LeapStatus::None), true)
1947            .unwrap();
1948        assert_eq!(filter.leap_vote(&config), Some(LeapStatus::None));
1949
1950        let filter = base_filter
1951            .clone()
1952            .external_data_update(link_1, 0.1, Some(LeapStatus::Leap59), true)
1953            .unwrap()
1954            .external_data_update(link_2, 0.1, Some(LeapStatus::Leap59), true)
1955            .unwrap()
1956            .external_data_update(link_3, 0.1, Some(LeapStatus::Leap59), true)
1957            .unwrap();
1958        assert_eq!(filter.leap_vote(&config), Some(LeapStatus::Leap59));
1959
1960        let filter = base_filter
1961            .clone()
1962            .external_data_update(link_1, 0.1, Some(LeapStatus::Leap61), true)
1963            .unwrap()
1964            .external_data_update(link_2, 0.1, Some(LeapStatus::Leap61), true)
1965            .unwrap()
1966            .external_data_update(link_3, 0.1, Some(LeapStatus::Leap61), true)
1967            .unwrap();
1968        assert_eq!(filter.leap_vote(&config), Some(LeapStatus::Leap61));
1969
1970        let filter = base_filter
1971            .clone()
1972            .external_data_update(link_1, 0.1, Some(LeapStatus::None), true)
1973            .unwrap()
1974            .external_data_update(link_2, 0.1, Some(LeapStatus::Leap59), true)
1975            .unwrap()
1976            .external_data_update(link_3, 0.1, Some(LeapStatus::Leap59), true)
1977            .unwrap();
1978        assert_eq!(filter.leap_vote(&config), Some(LeapStatus::Leap59));
1979
1980        let filter = base_filter
1981            .clone()
1982            .external_data_update(link_1, 0.1, Some(LeapStatus::None), true)
1983            .unwrap()
1984            .external_data_update(link_2, 0.1, None, true)
1985            .unwrap()
1986            .external_data_update(link_3, 0.1, None, true)
1987            .unwrap();
1988        assert_eq!(filter.leap_vote(&config), Some(LeapStatus::None));
1989
1990        let filter = base_filter
1991            .clone()
1992            .external_data_update(link_1, 0.1, Some(LeapStatus::None), true)
1993            .unwrap()
1994            .external_data_update(link_2, 0.1, Some(LeapStatus::Leap59), true)
1995            .unwrap()
1996            .external_data_update(link_3, 0.1, Some(LeapStatus::Leap61), true)
1997            .unwrap();
1998        assert_eq!(filter.leap_vote(&config), None);
1999    }
2000
2001    #[test]
2002    fn local_root_delay() {
2003        let config = ControllerConfig {
2004            select_offset_uncertainty_window: 2.0,
2005            select_link_uncertainty_window: 2.0,
2006            select_delay_uncertainty_window: 0.7,
2007            select_max_window_size: Duration::from_seconds_nanos(1, 0),
2008            minimum_agreeing_sources: 1,
2009        };
2010
2011        let filter = LinkFilter::<StdKalmanStorage<()>>::empty(Timestamp::UNIX_EPOCH);
2012        let (filter, clock_int_1) = filter
2013            .add_clock((0.0, 0.0).into(), (0.0, 0.0).into(), 1e-8)
2014            .unwrap();
2015        let (filter, clock_int_2) = filter
2016            .add_clock((0.0, 0.0).into(), (0.0, 0.0).into(), 1e-8)
2017            .unwrap();
2018        let (filter, link_1) = filter
2019            .add_untracked_link(clock_int_1, None, LinkConfig::default())
2020            .unwrap();
2021        let (filter, link_2) = filter
2022            .add_untracked_link(clock_int_1, None, LinkConfig::default())
2023            .unwrap();
2024        let (filter, link_3) = filter
2025            .add_untracked_link(clock_int_2, None, LinkConfig::default())
2026            .unwrap();
2027
2028        let filter = filter
2029            .external_data_update(link_1, 0.3, None, true)
2030            .unwrap()
2031            .measurement(&config, link_1.forward(), (0.0, 0.001).into(), clock_int_1)
2032            .unwrap()
2033            .external_data_update(link_2, 0.2, None, true)
2034            .unwrap()
2035            .measurement(&config, link_2.forward(), (0.0, 0.001).into(), clock_int_1)
2036            .unwrap()
2037            .external_data_update(link_3, 0.1, None, true)
2038            .unwrap()
2039            .measurement(&config, link_3.forward(), (10.0, 0.001).into(), clock_int_1)
2040            .unwrap();
2041
2042        assert_eq!(filter.local_root_delay(&config), Some(0.2));
2043
2044        let filter = filter.remove_link(link_2).unwrap();
2045
2046        assert_eq!(filter.local_root_delay(&config), None);
2047    }
2048
2049    #[test]
2050    fn test_new_link_validation() {
2051        let filter = LinkFilter::<StdKalmanStorage<()>>::empty(Timestamp::UNIX_EPOCH);
2052        let (filter, clock_1) = filter
2053            .add_clock((0.0, 0.0).into(), (0.0, 0.0).into(), 1e-8)
2054            .unwrap();
2055        let clock_2 = ClockId::new();
2056        let (filter, link_1) = filter
2057            .add_untracked_link(clock_1, None, LinkConfig::default())
2058            .unwrap();
2059        let clock_3 = link_1.second_clock();
2060
2061        assert_eq!(
2062            filter
2063                .clone()
2064                .add_untracked_link(clock_1, Some(clock_2), LinkConfig::default(),)
2065                .unwrap_err(),
2066            AlgoError::UnknownClock(clock_2)
2067        );
2068        assert_eq!(
2069            filter
2070                .clone()
2071                .add_untracked_link(clock_2, Some(clock_1), LinkConfig::default(),)
2072                .unwrap_err(),
2073            AlgoError::UnknownClock(clock_2)
2074        );
2075        assert_eq!(
2076            filter
2077                .clone()
2078                .add_untracked_link(clock_2, None, LinkConfig::default(),)
2079                .unwrap_err(),
2080            AlgoError::UnknownClock(clock_2)
2081        );
2082        assert_eq!(
2083            filter
2084                .clone()
2085                .add_untracked_link(clock_1, Some(clock_3), LinkConfig::default())
2086                .unwrap_err(),
2087            AlgoError::UnexpectedExternalClock(clock_3)
2088        );
2089        assert_eq!(
2090            filter
2091                .clone()
2092                .add_untracked_link(clock_3, Some(clock_1), LinkConfig::default())
2093                .unwrap_err(),
2094            AlgoError::UnexpectedExternalClock(clock_3)
2095        );
2096        assert_eq!(
2097            filter
2098                .clone()
2099                .add_untracked_link(clock_3, None, LinkConfig::default())
2100                .unwrap_err(),
2101            AlgoError::UnexpectedExternalClock(clock_3)
2102        );
2103
2104        assert_eq!(
2105            filter
2106                .clone()
2107                .add_tracked_link(
2108                    clock_1,
2109                    Some(clock_2),
2110                    LinkConfig::default(),
2111                    TrackedLinkConfig::default()
2112                )
2113                .unwrap_err(),
2114            AlgoError::UnknownClock(clock_2)
2115        );
2116        assert_eq!(
2117            filter
2118                .clone()
2119                .add_tracked_link(
2120                    clock_2,
2121                    Some(clock_1),
2122                    LinkConfig::default(),
2123                    TrackedLinkConfig::default()
2124                )
2125                .unwrap_err(),
2126            AlgoError::UnknownClock(clock_2)
2127        );
2128        assert_eq!(
2129            filter
2130                .clone()
2131                .add_tracked_link(
2132                    clock_2,
2133                    None,
2134                    LinkConfig::default(),
2135                    TrackedLinkConfig::default()
2136                )
2137                .unwrap_err(),
2138            AlgoError::UnknownClock(clock_2)
2139        );
2140        assert_eq!(
2141            filter
2142                .clone()
2143                .add_tracked_link(
2144                    clock_1,
2145                    Some(clock_3),
2146                    LinkConfig::default(),
2147                    TrackedLinkConfig::default()
2148                )
2149                .unwrap_err(),
2150            AlgoError::UnexpectedExternalClock(clock_3)
2151        );
2152        assert_eq!(
2153            filter
2154                .clone()
2155                .add_tracked_link(
2156                    clock_3,
2157                    Some(clock_1),
2158                    LinkConfig::default(),
2159                    TrackedLinkConfig::default()
2160                )
2161                .unwrap_err(),
2162            AlgoError::UnexpectedExternalClock(clock_3)
2163        );
2164        assert_eq!(
2165            filter
2166                .clone()
2167                .add_tracked_link(
2168                    clock_3,
2169                    None,
2170                    LinkConfig::default(),
2171                    TrackedLinkConfig::default()
2172                )
2173                .unwrap_err(),
2174            AlgoError::UnexpectedExternalClock(clock_3)
2175        );
2176    }
2177
2178    #[test]
2179    fn test_remove_clock_with_link_is_forbidden() {
2180        let filter = LinkFilter::<StdKalmanStorage<()>>::empty(Timestamp::UNIX_EPOCH);
2181        let (filter, clock_1) = filter
2182            .add_clock((0.0, 0.0).into(), (0.0, 0.0).into(), 0.0)
2183            .unwrap();
2184        let (filter, link_1) = filter
2185            .add_untracked_link(clock_1, None, LinkConfig::default())
2186            .unwrap();
2187
2188        assert_eq!(
2189            filter.clone().remove_clock(clock_1).unwrap_err(),
2190            AlgoError::ClockInUse(clock_1, link_1)
2191        );
2192
2193        let filter = LinkFilter::<StdKalmanStorage<()>>::empty(Timestamp::UNIX_EPOCH);
2194        let (filter, clock_1) = filter
2195            .add_clock((0.0, 0.0).into(), (0.0, 0.0).into(), 0.0)
2196            .unwrap();
2197        let (filter, link_1) = filter
2198            .add_tracked_link(
2199                clock_1,
2200                None,
2201                LinkConfig::default(),
2202                TrackedLinkConfig::default(),
2203            )
2204            .unwrap();
2205
2206        assert_eq!(
2207            filter.clone().remove_clock(clock_1).unwrap_err(),
2208            AlgoError::ClockInUse(clock_1, link_1)
2209        );
2210    }
2211
2212    #[test]
2213    fn test_remove_external_link_removes_external_clock() {
2214        let filter = LinkFilter::<StdKalmanStorage<()>>::empty(Timestamp::UNIX_EPOCH);
2215        let (filter, clock_1) = filter
2216            .add_clock((0.0, 0.0).into(), (0.0, 0.0).into(), 0.0)
2217            .unwrap();
2218        let (filter, link_1) = filter
2219            .add_untracked_link(clock_1, None, LinkConfig::default())
2220            .unwrap();
2221        let clock_2 = link_1.second_clock();
2222
2223        assert!(filter.estimation_state.is_external_clock(clock_2));
2224        let filter = filter.remove_link(link_1).unwrap();
2225        assert!(!filter.estimation_state.is_external_clock(clock_2));
2226    }
2227
2228    #[test]
2229    fn test_importance() {
2230        let config = ControllerConfig {
2231            select_offset_uncertainty_window: 2.0,
2232            select_link_uncertainty_window: 2.0,
2233            select_delay_uncertainty_window: 0.7,
2234            select_max_window_size: Duration::from_seconds_nanos(1, 0),
2235            minimum_agreeing_sources: 1,
2236        };
2237
2238        let filter = LinkFilter::<StdKalmanStorage<()>>::empty(Timestamp::UNIX_EPOCH);
2239        let (filter, clock_1) = filter
2240            .add_clock((0.0, 1e-3).into(), (0.0, 0.0).into(), 7.5e-12f64.sqrt())
2241            .unwrap();
2242        let (filter, link_1) = filter
2243            .add_untracked_link(clock_1, None, LinkConfig::default())
2244            .unwrap();
2245        let (filter, _link_2) = filter
2246            .add_untracked_link(clock_1, None, LinkConfig::default())
2247            .unwrap();
2248        let filter = filter
2249            .external_data_update(link_1, 0.0, None, true)
2250            .unwrap()
2251            .measurement(&config, link_1.forward(), (0.0, 1e-3).into(), clock_1)
2252            .unwrap();
2253        assert!(filter.link_active(link_1).unwrap());
2254        let importance = filter.link_importance(link_1).unwrap().unwrap();
2255        assert_almost_eq!(importance, 0.5);
2256        let filter = filter
2257            .progress_time(Timestamp::UNIX_EPOCH + Duration::from_seconds_nanos(100, 0))
2258            .unwrap()
2259            .measurement(&config, link_1.forward(), (0.0, 1e-3).into(), clock_1)
2260            .unwrap();
2261        let importance = filter.link_importance(link_1).unwrap().unwrap();
2262        assert_almost_eq!(importance, 0.625);
2263
2264        let active_links = filter.active_links();
2265        assert_eq!(active_links.len(), 1);
2266        assert_eq!(active_links[0].id, link_1);
2267        assert_almost_eq!(active_links[0].importance, 0.625);
2268    }
2269}