1use std::collections::BTreeMap;
54
55use crate::carrier_phase::{
56 CarrierPhaseError, SlipReason, DEFAULT_GF_THRESHOLD_M, DEFAULT_MIN_ARC_GAP_S,
57 DEFAULT_MW_THRESHOLD_CYCLES,
58};
59use crate::frequencies::wavelength_for_frequency;
60use crate::tolerances::FREQUENCY_DENOMINATOR_EPS_HZ;
61
62use super::prep::DualFrequencyObservation;
63
64pub const DEFAULT_MINIMUM_ARC_LENGTH: usize = 2;
66
67pub const DEFAULT_RUNNING_STATISTIC_K_FACTOR: f64 = 4.0;
69
70#[derive(Debug, Clone, Copy, PartialEq)]
72pub struct CycleSlipConfig {
73 pub melbourne_wubbena_threshold_cycles: f64,
75 pub geometry_free_threshold_m: f64,
77 pub minimum_arc_length: usize,
79 pub running_statistic_k_factor: f64,
81 pub maximum_gap_s: f64,
83}
84
85impl CycleSlipConfig {
86 pub fn validate(&self) -> Result<(), CycleSlipConfigError> {
88 validate_positive_finite(
89 self.melbourne_wubbena_threshold_cycles,
90 CycleSlipConfigError::InvalidMelbourneWubbenaThreshold,
91 )?;
92 validate_positive_finite(
93 self.geometry_free_threshold_m,
94 CycleSlipConfigError::InvalidGeometryFreeThreshold,
95 )?;
96 validate_positive_finite(
97 self.running_statistic_k_factor,
98 CycleSlipConfigError::InvalidRunningStatisticKFactor,
99 )?;
100 validate_positive_finite(self.maximum_gap_s, CycleSlipConfigError::InvalidMaximumGap)?;
101 if self.minimum_arc_length == 0 {
102 return Err(CycleSlipConfigError::InvalidMinimumArcLength);
103 }
104 Ok(())
105 }
106}
107
108impl Default for CycleSlipConfig {
109 fn default() -> Self {
110 Self {
111 melbourne_wubbena_threshold_cycles: DEFAULT_MW_THRESHOLD_CYCLES,
112 geometry_free_threshold_m: DEFAULT_GF_THRESHOLD_M,
113 minimum_arc_length: DEFAULT_MINIMUM_ARC_LENGTH,
114 running_statistic_k_factor: DEFAULT_RUNNING_STATISTIC_K_FACTOR,
115 maximum_gap_s: DEFAULT_MIN_ARC_GAP_S,
116 }
117 }
118}
119
120#[derive(Debug, Clone, Copy, PartialEq, Eq)]
122pub enum CycleSlipConfigError {
123 InvalidMelbourneWubbenaThreshold,
125 InvalidGeometryFreeThreshold,
127 InvalidMinimumArcLength,
129 InvalidRunningStatisticKFactor,
131 InvalidMaximumGap,
133}
134
135impl core::fmt::Display for CycleSlipConfigError {
136 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
137 match self {
138 Self::InvalidMelbourneWubbenaThreshold => {
139 write!(f, "Melbourne-Wubbena threshold must be positive and finite")
140 }
141 Self::InvalidGeometryFreeThreshold => {
142 write!(f, "geometry-free threshold must be positive and finite")
143 }
144 Self::InvalidMinimumArcLength => write!(f, "minimum arc length must be nonzero"),
145 Self::InvalidRunningStatisticKFactor => {
146 write!(f, "running-statistic k-factor must be positive and finite")
147 }
148 Self::InvalidMaximumGap => write!(f, "maximum gap must be positive and finite"),
149 }
150 }
151}
152
153impl std::error::Error for CycleSlipConfigError {}
154
155#[derive(Debug, Clone, Copy, PartialEq, Eq)]
157pub enum CycleSlipError {
158 InvalidConfig(CycleSlipConfigError),
160 NonFiniteObservation,
162 NonFiniteEpochTime,
164 EpochsNotOrdered,
166 InvalidFrequency,
168 EqualFrequencies,
170}
171
172impl core::fmt::Display for CycleSlipError {
173 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
174 match self {
175 Self::InvalidConfig(err) => write!(f, "invalid cycle-slip config: {err}"),
176 Self::NonFiniteObservation => write!(f, "cycle-slip observation must be finite"),
177 Self::NonFiniteEpochTime => write!(f, "cycle-slip epoch time must be finite"),
178 Self::EpochsNotOrdered => write!(f, "cycle-slip epochs must be time ordered"),
179 Self::InvalidFrequency => write!(f, "carrier frequency must be positive and finite"),
180 Self::EqualFrequencies => write!(f, "carrier frequencies must be distinct"),
181 }
182 }
183}
184
185impl std::error::Error for CycleSlipError {}
186
187impl From<CycleSlipConfigError> for CycleSlipError {
188 fn from(err: CycleSlipConfigError) -> Self {
189 Self::InvalidConfig(err)
190 }
191}
192
193impl From<CarrierPhaseError> for CycleSlipError {
194 fn from(err: CarrierPhaseError) -> Self {
195 match err {
196 CarrierPhaseError::EqualFrequencies => Self::EqualFrequencies,
197 CarrierPhaseError::InvalidFrequency => Self::InvalidFrequency,
198 CarrierPhaseError::InvalidObservation => Self::NonFiniteObservation,
199 CarrierPhaseError::InvalidThreshold => {
200 Self::InvalidConfig(CycleSlipConfigError::InvalidMelbourneWubbenaThreshold)
201 }
202 }
203 }
204}
205
206#[derive(Debug, Clone, Copy, PartialEq)]
208pub struct RunningMeanVariance {
209 pub sample_count: usize,
211 pub mean: f64,
213 pub variance: f64,
215}
216
217impl RunningMeanVariance {
218 pub const fn new() -> Self {
220 Self {
221 sample_count: 0,
222 mean: 0.0,
223 variance: 0.0,
224 }
225 }
226
227 pub fn push(&mut self, value: f64) {
229 let previous_count = self.sample_count;
230 self.sample_count += 1;
231
232 if previous_count == 0 {
233 self.mean = value;
234 self.variance = 0.0;
235 return;
236 }
237
238 let previous_mean = self.mean;
239 let previous_m2 = self.variance * previous_count.saturating_sub(1) as f64;
240 let delta = value - previous_mean;
241 self.mean = previous_mean + delta / self.sample_count as f64;
242 let delta_after_mean_update = value - self.mean;
243 let m2 = previous_m2 + delta * delta_after_mean_update;
244 self.variance = m2 / (self.sample_count - 1) as f64;
245 }
246
247 pub fn standard_deviation(&self) -> Option<f64> {
249 if self.sample_count >= 2 && self.variance.is_finite() && self.variance >= 0.0 {
250 Some(self.variance.sqrt())
251 } else {
252 None
253 }
254 }
255}
256
257impl Default for RunningMeanVariance {
258 fn default() -> Self {
259 Self::new()
260 }
261}
262
263#[derive(Debug, Clone, Copy, PartialEq)]
265pub struct SatelliteCycleSlipState {
266 pub previous_epoch_time_s: Option<f64>,
268 pub previous_melbourne_wubbena_cycles: Option<f64>,
270 pub melbourne_wubbena: RunningMeanVariance,
272 pub previous_geometry_free_m: Option<f64>,
274}
275
276impl SatelliteCycleSlipState {
277 pub const fn new() -> Self {
279 Self {
280 previous_epoch_time_s: None,
281 previous_melbourne_wubbena_cycles: None,
282 melbourne_wubbena: RunningMeanVariance::new(),
283 previous_geometry_free_m: None,
284 }
285 }
286
287 pub fn reset_arc(&mut self) {
289 *self = Self::new();
290 }
291}
292
293impl Default for SatelliteCycleSlipState {
294 fn default() -> Self {
295 Self::new()
296 }
297}
298
299pub type CycleSlipStateKey = (String, String);
301
302#[derive(Debug, Clone, PartialEq)]
304pub struct CycleSlipDetectorState {
305 pub satellites: BTreeMap<CycleSlipStateKey, SatelliteCycleSlipState>,
307}
308
309impl CycleSlipDetectorState {
310 pub fn new() -> Self {
312 Self {
313 satellites: BTreeMap::new(),
314 }
315 }
316}
317
318impl Default for CycleSlipDetectorState {
319 fn default() -> Self {
320 Self::new()
321 }
322}
323
324#[derive(Debug, Clone, Copy, PartialEq)]
326pub struct MelbourneWubbenaUpdate {
327 pub melbourne_wubbena_cycles: f64,
329 pub slip: bool,
331}
332
333#[derive(Debug, Clone, Copy, PartialEq)]
335pub struct GeometryFreeUpdate {
336 pub geometry_free_m: f64,
338 pub slip: bool,
340 pub reset: bool,
342}
343
344#[derive(Debug, Clone, PartialEq, Eq)]
346pub struct CycleSlipFlagObservation {
347 pub satellite_id: String,
349 pub slip: bool,
351 pub reasons: Vec<SlipReason>,
353}
354
355#[derive(Debug, Clone, PartialEq)]
357pub struct CycleSlipFlagEpoch {
358 pub gap_time_s: Option<f64>,
360 pub observations: Vec<CycleSlipFlagObservation>,
362}
363
364pub fn melbourne_wubbena_cycles(
366 observation: &DualFrequencyObservation,
367) -> Result<f64, CycleSlipError> {
368 validate_observation_finite(observation)?;
369 let lambda_wl = wide_lane_wavelength_m(observation.f1_hz, observation.f2_hz)?;
370 let narrow_lane_code_m = narrow_lane_code_m(observation)?;
371 let wide_lane_phase_m = lambda_wl * (observation.phi1_cyc - observation.phi2_cyc);
372 Ok((wide_lane_phase_m - narrow_lane_code_m) / lambda_wl)
373}
374
375pub fn update_melbourne_wubbena(
377 state: &mut SatelliteCycleSlipState,
378 observation: &DualFrequencyObservation,
379 config: CycleSlipConfig,
380) -> Result<MelbourneWubbenaUpdate, CycleSlipError> {
381 config.validate()?;
382 let mw_cycles = melbourne_wubbena_cycles(observation)?;
383 let slip = melbourne_wubbena_slip(state, mw_cycles, config);
384
385 state.previous_melbourne_wubbena_cycles = Some(mw_cycles);
386 state.melbourne_wubbena.push(mw_cycles);
387
388 Ok(MelbourneWubbenaUpdate {
389 melbourne_wubbena_cycles: mw_cycles,
390 slip,
391 })
392}
393
394pub fn geometry_free_m(observation: &DualFrequencyObservation) -> Result<f64, CycleSlipError> {
396 validate_observation_finite(observation)?;
397 let l1_m = phase_meters(observation.phi1_cyc, observation.f1_hz)?;
398 let l2_m = phase_meters(observation.phi2_cyc, observation.f2_hz)?;
399 Ok(l1_m - l2_m)
400}
401
402pub fn update_geometry_free(
404 state: &mut SatelliteCycleSlipState,
405 observation: &DualFrequencyObservation,
406 epoch_time_s: Option<f64>,
407 config: CycleSlipConfig,
408) -> Result<GeometryFreeUpdate, CycleSlipError> {
409 config.validate()?;
410 validate_epoch_time(epoch_time_s)?;
411 if epoch_time_goes_back(state.previous_epoch_time_s, epoch_time_s) {
412 return Err(CycleSlipError::EpochsNotOrdered);
413 }
414 let gf_m = geometry_free_m(observation)?;
415 let reset = gap_reset(
416 state.previous_epoch_time_s,
417 epoch_time_s,
418 config.maximum_gap_s,
419 );
420 if reset {
421 state.reset_arc();
422 }
423 let slip = !reset
424 && state
425 .previous_geometry_free_m
426 .is_some_and(|prev| (gf_m - prev).abs() > config.geometry_free_threshold_m);
427
428 remember_epoch_time(state, epoch_time_s);
429 state.previous_geometry_free_m = Some(gf_m);
430
431 Ok(GeometryFreeUpdate {
432 geometry_free_m: gf_m,
433 slip,
434 reset,
435 })
436}
437
438pub fn detect_cycle_slips(
440 epochs: &[super::prep::DualFrequencyEpoch],
441 config: CycleSlipConfig,
442) -> Result<Vec<CycleSlipFlagEpoch>, CycleSlipError> {
443 config.validate()?;
444 validate_epoch_order(epochs)?;
445 let mut state = CycleSlipDetectorState::new();
446 let mut out = Vec::with_capacity(epochs.len());
447
448 for epoch in epochs {
449 let observations = epoch
450 .observations
451 .iter()
452 .map(|observation| {
453 let ambiguity_state = state
454 .satellites
455 .entry(cycle_slip_state_key(observation))
456 .or_default();
457 classify_dual_frequency_observation(
458 ambiguity_state,
459 observation,
460 epoch.gap_time_s,
461 config,
462 )
463 })
464 .collect::<Result<Vec<_>, _>>()?;
465 out.push(CycleSlipFlagEpoch {
466 gap_time_s: epoch.gap_time_s,
467 observations,
468 });
469 }
470
471 Ok(out)
472}
473
474fn cycle_slip_state_key(observation: &DualFrequencyObservation) -> CycleSlipStateKey {
475 (
476 observation.satellite_id.clone(),
477 observation.ambiguity_id.clone(),
478 )
479}
480
481fn validate_positive_finite(
482 value: f64,
483 error: CycleSlipConfigError,
484) -> Result<(), CycleSlipConfigError> {
485 if value.is_finite() && value > 0.0 {
486 Ok(())
487 } else {
488 Err(error)
489 }
490}
491
492fn validate_observation_finite(
493 observation: &DualFrequencyObservation,
494) -> Result<(), CycleSlipError> {
495 if observation.p1_m.is_finite()
496 && observation.p2_m.is_finite()
497 && observation.phi1_cyc.is_finite()
498 && observation.phi2_cyc.is_finite()
499 && observation.f1_hz.is_finite()
500 && observation.f2_hz.is_finite()
501 {
502 Ok(())
503 } else {
504 Err(CycleSlipError::NonFiniteObservation)
505 }
506}
507
508fn validate_epoch_time(epoch_time_s: Option<f64>) -> Result<(), CycleSlipError> {
509 if epoch_time_s.is_some_and(|time_s| !time_s.is_finite()) {
510 Err(CycleSlipError::NonFiniteEpochTime)
511 } else {
512 Ok(())
513 }
514}
515
516fn validate_epoch_order(epochs: &[super::prep::DualFrequencyEpoch]) -> Result<(), CycleSlipError> {
517 let mut previous_time_s = None;
518 for epoch in epochs {
519 validate_epoch_time(epoch.gap_time_s)?;
520 if let (Some(previous), Some(current)) = (previous_time_s, epoch.gap_time_s) {
521 if current < previous {
522 return Err(CycleSlipError::EpochsNotOrdered);
523 }
524 }
525 if epoch.gap_time_s.is_some() {
526 previous_time_s = epoch.gap_time_s;
527 }
528 }
529 Ok(())
530}
531
532fn phase_meters(phi_cycles: f64, f_hz: f64) -> Result<f64, CycleSlipError> {
533 validate_frequency(f_hz)?;
534 let wavelength_m = wavelength_for_frequency(f_hz).ok_or(CycleSlipError::InvalidFrequency)?;
535 let phase_m = wavelength_m * phi_cycles;
536 if phase_m.is_finite() {
537 Ok(phase_m)
538 } else {
539 Err(CycleSlipError::NonFiniteObservation)
540 }
541}
542
543fn validate_frequency(f_hz: f64) -> Result<(), CycleSlipError> {
544 if f_hz.is_finite() && f_hz > 0.0 {
545 Ok(())
546 } else {
547 Err(CycleSlipError::InvalidFrequency)
548 }
549}
550
551fn wide_lane_wavelength_m(f1_hz: f64, f2_hz: f64) -> Result<f64, CycleSlipError> {
552 validate_frequency(f1_hz)?;
553 validate_frequency(f2_hz)?;
554 let lambda1 = wavelength_for_frequency(f1_hz).ok_or(CycleSlipError::InvalidFrequency)?;
555 let lambda2 = wavelength_for_frequency(f2_hz).ok_or(CycleSlipError::InvalidFrequency)?;
556 let inverse_wide_lane = 1.0 / lambda1 - 1.0 / lambda2;
557 if inverse_wide_lane.abs() * crate::constants::C_M_S < FREQUENCY_DENOMINATOR_EPS_HZ {
558 Err(CycleSlipError::EqualFrequencies)
559 } else {
560 Ok(1.0 / inverse_wide_lane)
561 }
562}
563
564fn narrow_lane_code_m(observation: &DualFrequencyObservation) -> Result<f64, CycleSlipError> {
565 let f1_hz = observation.f1_hz;
566 let f2_hz = observation.f2_hz;
567 validate_frequency(f1_hz)?;
568 validate_frequency(f2_hz)?;
569 if wavelength_for_frequency(f1_hz).is_none() || wavelength_for_frequency(f2_hz).is_none() {
570 return Err(CycleSlipError::InvalidFrequency);
571 }
572 let denominator = f1_hz + f2_hz;
573 if denominator.abs() < FREQUENCY_DENOMINATOR_EPS_HZ {
574 Err(CycleSlipError::EqualFrequencies)
575 } else {
576 Ok((f1_hz * observation.p1_m + f2_hz * observation.p2_m) / denominator)
577 }
578}
579
580fn melbourne_wubbena_slip(
581 state: &SatelliteCycleSlipState,
582 mw_cycles: f64,
583 config: CycleSlipConfig,
584) -> bool {
585 let step_slip = state
586 .previous_melbourne_wubbena_cycles
587 .is_some_and(|prev| (mw_cycles - prev).abs() > config.melbourne_wubbena_threshold_cycles);
588 let sigma_slip = state.melbourne_wubbena.sample_count >= config.minimum_arc_length
589 && state
590 .melbourne_wubbena
591 .standard_deviation()
592 .is_some_and(|sigma| {
593 sigma > 0.0
594 && (mw_cycles - state.melbourne_wubbena.mean).abs()
595 > config.running_statistic_k_factor * sigma
596 });
597 step_slip || sigma_slip
598}
599
600fn epoch_time_goes_back(prev_time_s: Option<f64>, time_s: Option<f64>) -> bool {
601 matches!(
602 (prev_time_s, time_s),
603 (Some(previous), Some(current)) if current < previous
604 )
605}
606
607fn gap_reset(prev_time_s: Option<f64>, time_s: Option<f64>, maximum_gap_s: f64) -> bool {
608 match (prev_time_s, time_s) {
609 (Some(prev), Some(current)) => current - prev > maximum_gap_s,
610 _ => false,
611 }
612}
613
614fn remember_epoch_time(state: &mut SatelliteCycleSlipState, epoch_time_s: Option<f64>) {
615 if let Some(time_s) = epoch_time_s {
616 state.previous_epoch_time_s = Some(time_s);
617 }
618}
619
620fn lli_slip(observation: &DualFrequencyObservation) -> bool {
621 lli_bit0_set(observation.lli1) || lli_bit0_set(observation.lli2)
622}
623
624fn lli_bit0_set(lli: Option<i64>) -> bool {
625 lli.is_some_and(|value| (value & 1) == 1)
626}
627
628fn classify_dual_frequency_observation(
629 state: &mut SatelliteCycleSlipState,
630 observation: &DualFrequencyObservation,
631 epoch_time_s: Option<f64>,
632 config: CycleSlipConfig,
633) -> Result<CycleSlipFlagObservation, CycleSlipError> {
634 let reset = gap_reset(
635 state.previous_epoch_time_s,
636 epoch_time_s,
637 config.maximum_gap_s,
638 );
639 if reset {
640 state.reset_arc();
641 }
642
643 let mw_cycles = melbourne_wubbena_cycles(observation)?;
644 let gf_m = geometry_free_m(observation)?;
645 let mut reasons = Vec::new();
646 let lli = lli_slip(observation);
647 if lli {
648 reasons.push(SlipReason::Lli);
649 state.reset_arc();
650 }
651 if !reset && !lli {
652 if state
653 .previous_geometry_free_m
654 .is_some_and(|prev| (gf_m - prev).abs() > config.geometry_free_threshold_m)
655 {
656 reasons.push(SlipReason::GeometryFree);
657 }
658 if melbourne_wubbena_slip(state, mw_cycles, config) {
659 reasons.push(SlipReason::MelbourneWubbena);
660 }
661 }
662
663 remember_epoch_time(state, epoch_time_s);
664 state.previous_geometry_free_m = Some(gf_m);
665 state.previous_melbourne_wubbena_cycles = Some(mw_cycles);
666 state.melbourne_wubbena.push(mw_cycles);
667
668 Ok(CycleSlipFlagObservation {
669 satellite_id: observation.satellite_id.clone(),
670 slip: !reasons.is_empty(),
671 reasons,
672 })
673}
674
675#[cfg(test)]
676mod tests {
677 use super::*;
678 use crate::constants::{C_M_S, F_L1_HZ, F_L2_HZ};
679
680 #[test]
681 fn cycle_slip_types_construct() {
682 let config = CycleSlipConfig {
683 melbourne_wubbena_threshold_cycles: 3.0,
684 geometry_free_threshold_m: 0.04,
685 minimum_arc_length: 3,
686 running_statistic_k_factor: 5.0,
687 maximum_gap_s: 120.0,
688 };
689 let satellite = SatelliteCycleSlipState {
690 previous_epoch_time_s: Some(30.0),
691 previous_melbourne_wubbena_cycles: Some(12.0),
692 melbourne_wubbena: RunningMeanVariance {
693 sample_count: 4,
694 mean: 12.0,
695 variance: 0.01,
696 },
697 previous_geometry_free_m: Some(0.2),
698 };
699 let mut detector = CycleSlipDetectorState::new();
700 detector
701 .satellites
702 .insert(("G01".to_string(), "G01:L1L2".to_string()), satellite);
703
704 assert_eq!(config.minimum_arc_length, 3);
705 assert_eq!(
706 detector
707 .satellites
708 .get(&("G01".to_string(), "G01:L1L2".to_string())),
709 Some(&SatelliteCycleSlipState {
710 previous_epoch_time_s: Some(30.0),
711 previous_melbourne_wubbena_cycles: Some(12.0),
712 melbourne_wubbena: RunningMeanVariance {
713 sample_count: 4,
714 mean: 12.0,
715 variance: 0.01,
716 },
717 previous_geometry_free_m: Some(0.2),
718 })
719 );
720 }
721
722 #[test]
723 fn cycle_slip_default_config_is_well_formed() {
724 let config = CycleSlipConfig::default();
725
726 assert_eq!(
727 config.melbourne_wubbena_threshold_cycles,
728 DEFAULT_MW_THRESHOLD_CYCLES
729 );
730 assert_eq!(config.geometry_free_threshold_m, DEFAULT_GF_THRESHOLD_M);
731 assert_eq!(config.maximum_gap_s, DEFAULT_MIN_ARC_GAP_S);
732 assert!(config.validate().is_ok());
733 }
734
735 #[test]
736 fn cycle_slip_config_rejects_nonsensical_thresholds() {
737 assert_eq!(
738 CycleSlipConfig {
739 melbourne_wubbena_threshold_cycles: 0.0,
740 ..CycleSlipConfig::default()
741 }
742 .validate(),
743 Err(CycleSlipConfigError::InvalidMelbourneWubbenaThreshold)
744 );
745 assert_eq!(
746 CycleSlipConfig {
747 geometry_free_threshold_m: f64::INFINITY,
748 ..CycleSlipConfig::default()
749 }
750 .validate(),
751 Err(CycleSlipConfigError::InvalidGeometryFreeThreshold)
752 );
753 assert_eq!(
754 CycleSlipConfig {
755 minimum_arc_length: 0,
756 ..CycleSlipConfig::default()
757 }
758 .validate(),
759 Err(CycleSlipConfigError::InvalidMinimumArcLength)
760 );
761 assert_eq!(
762 CycleSlipConfig {
763 running_statistic_k_factor: f64::NAN,
764 ..CycleSlipConfig::default()
765 }
766 .validate(),
767 Err(CycleSlipConfigError::InvalidRunningStatisticKFactor)
768 );
769 assert_eq!(
770 CycleSlipConfig {
771 maximum_gap_s: f64::NAN,
772 ..CycleSlipConfig::default()
773 }
774 .validate(),
775 Err(CycleSlipConfigError::InvalidMaximumGap)
776 );
777 }
778
779 #[test]
780 fn cycle_slip_clean_constant_mw_series_produces_no_slip() {
781 let mut state = SatelliteCycleSlipState::new();
782 let config = CycleSlipConfig {
783 melbourne_wubbena_threshold_cycles: 2.0,
784 minimum_arc_length: 2,
785 running_statistic_k_factor: 3.0,
786 ..CycleSlipConfig::default()
787 };
788 let updates = (0..5)
789 .map(|epoch| {
790 update_melbourne_wubbena(&mut state, &mw_observation("G01", epoch, 8.0), config)
791 .expect("MW update")
792 })
793 .collect::<Vec<_>>();
794
795 assert!(updates.iter().all(|update| !update.slip));
796 assert_eq!(state.melbourne_wubbena.sample_count, 5);
797 }
798
799 #[test]
800 fn cycle_slip_injected_widelane_integer_jump_is_flagged() {
801 let mut state = SatelliteCycleSlipState::new();
802 let config = CycleSlipConfig {
803 melbourne_wubbena_threshold_cycles: 2.0,
804 minimum_arc_length: 2,
805 running_statistic_k_factor: 3.0,
806 ..CycleSlipConfig::default()
807 };
808 let slips = (0..5)
809 .map(|epoch| {
810 let wide_lane_cycles = if epoch >= 3 { 12.0 } else { 8.0 };
811 update_melbourne_wubbena(
812 &mut state,
813 &mw_observation("G01", epoch, wide_lane_cycles),
814 config,
815 )
816 .expect("MW update")
817 .slip
818 })
819 .collect::<Vec<_>>();
820
821 assert_eq!(slips, vec![false, false, false, true, false]);
822 }
823
824 #[test]
825 fn cycle_slip_smooth_geometry_free_variation_does_not_flag() {
826 let mut state = SatelliteCycleSlipState::new();
827 let config = CycleSlipConfig {
828 geometry_free_threshold_m: 0.05,
829 maximum_gap_s: 120.0,
830 ..CycleSlipConfig::default()
831 };
832 let slips = (0..5)
833 .map(|epoch| {
834 update_geometry_free(
835 &mut state,
836 &gf_observation("G01", epoch, epoch as f64 * 0.01),
837 Some(epoch as f64 * 30.0),
838 config,
839 )
840 .expect("GF update")
841 .slip
842 })
843 .collect::<Vec<_>>();
844
845 assert_eq!(slips, vec![false; 5]);
846 }
847
848 #[test]
849 fn cycle_slip_geometry_free_phase_jump_on_one_frequency_flags() {
850 let mut state = SatelliteCycleSlipState::new();
851 let config = CycleSlipConfig {
852 geometry_free_threshold_m: 0.05,
853 maximum_gap_s: 120.0,
854 ..CycleSlipConfig::default()
855 };
856 let slips = (0..5)
857 .map(|epoch| {
858 let gf_m = if epoch >= 3 {
859 0.03 + 0.30
860 } else {
861 epoch as f64 * 0.01
862 };
863 update_geometry_free(
864 &mut state,
865 &gf_observation("G01", epoch, gf_m),
866 Some(epoch as f64 * 30.0),
867 config,
868 )
869 .expect("GF update")
870 .slip
871 })
872 .collect::<Vec<_>>();
873
874 assert_eq!(slips, vec![false, false, false, true, false]);
875 }
876
877 #[test]
878 fn cycle_slip_long_data_gap_resets_geometry_free_arc() {
879 let mut state = SatelliteCycleSlipState::new();
880 let config = CycleSlipConfig {
881 geometry_free_threshold_m: 0.05,
882 maximum_gap_s: 120.0,
883 ..CycleSlipConfig::default()
884 };
885 let samples = [
886 (0.0, 0.00),
887 (30.0, 0.01),
888 (60.0, 0.02),
889 (600.0, 0.50),
890 (630.0, 0.51),
891 ];
892 let updates = samples
893 .iter()
894 .enumerate()
895 .map(|(idx, (time_s, gf_m))| {
896 update_geometry_free(
897 &mut state,
898 &gf_observation("G01", idx, *gf_m),
899 Some(*time_s),
900 config,
901 )
902 .expect("GF update")
903 })
904 .collect::<Vec<_>>();
905
906 assert_eq!(
907 updates.iter().map(|update| update.slip).collect::<Vec<_>>(),
908 vec![false; 5]
909 );
910 assert_eq!(
911 updates
912 .iter()
913 .map(|update| update.reset)
914 .collect::<Vec<_>>(),
915 vec![false, false, false, true, false]
916 );
917 }
918
919 #[test]
920 fn cycle_slip_geometry_free_rejects_backward_timestamp_without_resetting_arc() {
921 let mut state = SatelliteCycleSlipState::new();
922 let config = CycleSlipConfig {
923 geometry_free_threshold_m: 0.05,
924 maximum_gap_s: 120.0,
925 ..CycleSlipConfig::default()
926 };
927
928 update_geometry_free(
929 &mut state,
930 &gf_observation("G01", 0, 0.01),
931 Some(30.0),
932 config,
933 )
934 .expect("initial GF update");
935
936 let err = update_geometry_free(
937 &mut state,
938 &gf_observation("G01", 1, 0.50),
939 Some(0.0),
940 config,
941 )
942 .expect_err("backward timestamp");
943
944 assert_eq!(err, CycleSlipError::EpochsNotOrdered);
945 assert_eq!(state.previous_epoch_time_s, Some(30.0));
946 assert_close(state.previous_geometry_free_m.expect("GF state"), 0.01);
947 }
948
949 #[test]
950 fn cycle_slip_geometry_free_forward_gap_still_resets_arc() {
951 let mut state = SatelliteCycleSlipState::new();
952 let config = CycleSlipConfig {
953 geometry_free_threshold_m: 0.05,
954 maximum_gap_s: 120.0,
955 ..CycleSlipConfig::default()
956 };
957
958 update_geometry_free(
959 &mut state,
960 &gf_observation("G01", 0, 0.01),
961 Some(30.0),
962 config,
963 )
964 .expect("initial GF update");
965 let update = update_geometry_free(
966 &mut state,
967 &gf_observation("G01", 1, 0.50),
968 Some(200.0),
969 config,
970 )
971 .expect("forward gap");
972
973 assert!(update.reset);
974 assert!(!update.slip);
975 assert_eq!(state.previous_epoch_time_s, Some(200.0));
976 assert_close(state.previous_geometry_free_m.expect("GF state"), 0.50);
977 }
978
979 #[test]
980 fn cycle_slip_driver_flags_exact_satellite_and_epoch() {
981 let epochs = (0..5)
982 .map(|epoch| super::super::prep::DualFrequencyEpoch {
983 gap_time_s: Some(epoch as f64 * 30.0),
984 observations: vec![
985 combined_observation("G01", epoch, 5.0, epoch as f64 * 0.01),
986 combined_observation(
987 "G02",
988 epoch,
989 if epoch >= 3 { 11.0 } else { 7.0 },
990 epoch as f64 * 0.01,
991 ),
992 ],
993 })
994 .collect::<Vec<_>>();
995 let config = CycleSlipConfig {
996 melbourne_wubbena_threshold_cycles: 2.0,
997 geometry_free_threshold_m: 0.05,
998 maximum_gap_s: 120.0,
999 ..CycleSlipConfig::default()
1000 };
1001
1002 let flags = detect_cycle_slips(&epochs, config).expect("cycle-slip driver");
1003 let slipped = slipped_epoch_satellites(&flags);
1004
1005 assert_eq!(
1006 slipped,
1007 vec![(3, "G02".to_string(), vec![SlipReason::MelbourneWubbena])]
1008 );
1009 }
1010
1011 #[test]
1012 fn cycle_slip_driver_keeps_same_satellite_ambiguities_independent() {
1013 let epochs = (0..5)
1014 .map(|epoch| super::super::prep::DualFrequencyEpoch {
1015 gap_time_s: Some(epoch as f64 * 30.0),
1016 observations: vec![
1017 combined_observation_with_ambiguity(
1018 "G01",
1019 "G01:L1L2",
1020 epoch,
1021 if epoch >= 3 { 11.0 } else { 7.0 },
1022 epoch as f64 * 0.01,
1023 ),
1024 combined_observation_with_ambiguity(
1025 "G01",
1026 "G01:L1L5",
1027 epoch,
1028 7.0,
1029 epoch as f64 * 0.01,
1030 ),
1031 ],
1032 })
1033 .collect::<Vec<_>>();
1034 let config = CycleSlipConfig {
1035 melbourne_wubbena_threshold_cycles: 2.0,
1036 geometry_free_threshold_m: 0.05,
1037 maximum_gap_s: 120.0,
1038 ..CycleSlipConfig::default()
1039 };
1040
1041 let flags = detect_cycle_slips(&epochs, config).expect("cycle-slip driver");
1042 let slipped = slipped_epoch_observations(&flags);
1043
1044 assert_eq!(
1045 slipped,
1046 vec![(3, 0, "G01".to_string(), vec![SlipReason::MelbourneWubbena])]
1047 );
1048 assert_eq!(flags[3].observations[0].satellite_id, "G01");
1049 assert_eq!(flags[3].observations[1].satellite_id, "G01");
1050 }
1051
1052 #[test]
1053 fn cycle_slip_driver_keeps_shared_ambiguity_different_satellites_independent() {
1054 let epochs = (0..=10)
1055 .map(|epoch| {
1056 let mut observations = Vec::new();
1057 if epoch == 0 {
1058 observations.push(combined_observation_with_ambiguity(
1059 "G01",
1060 "shared-ambiguity",
1061 epoch,
1062 5.0,
1063 0.0,
1064 ));
1065 } else if epoch == 10 {
1066 observations.push(combined_observation_with_ambiguity(
1067 "G01",
1068 "shared-ambiguity",
1069 epoch,
1070 12.0,
1071 0.80,
1072 ));
1073 }
1074 observations.push(combined_observation_with_ambiguity(
1075 "G02",
1076 "shared-ambiguity",
1077 epoch,
1078 5.0,
1079 epoch as f64 * 0.005,
1080 ));
1081 super::super::prep::DualFrequencyEpoch {
1082 gap_time_s: Some(epoch as f64 * 30.0),
1083 observations,
1084 }
1085 })
1086 .collect::<Vec<_>>();
1087 let config = CycleSlipConfig {
1088 melbourne_wubbena_threshold_cycles: 2.0,
1089 geometry_free_threshold_m: 0.05,
1090 maximum_gap_s: 120.0,
1091 ..CycleSlipConfig::default()
1092 };
1093
1094 let flags = detect_cycle_slips(&epochs, config).expect("cycle-slip driver");
1095
1096 assert!(slipped_epoch_observations(&flags).is_empty());
1097 assert_eq!(flags[10].observations[0].satellite_id, "G01");
1098 assert_eq!(flags[10].observations[1].satellite_id, "G02");
1099 }
1100
1101 #[test]
1102 fn cycle_slip_driver_preserves_input_observation_order() {
1103 let epochs = vec![super::super::prep::DualFrequencyEpoch {
1104 gap_time_s: Some(0.0),
1105 observations: vec![
1106 combined_observation("G03", 0, 5.0, 0.0),
1107 combined_observation("G01", 0, 5.0, 0.0),
1108 combined_observation("G02", 0, 5.0, 0.0),
1109 ],
1110 }];
1111
1112 let flags = detect_cycle_slips(&epochs, quiet_cycle_slip_config()).expect("cycle slips");
1113 let satellite_ids = flags[0]
1114 .observations
1115 .iter()
1116 .map(|observation| observation.satellite_id.as_str())
1117 .collect::<Vec<_>>();
1118
1119 assert_eq!(satellite_ids, vec!["G03", "G01", "G02"]);
1120 }
1121
1122 #[test]
1123 fn cycle_slip_driver_flags_lli_bit_without_combination_jump() {
1124 let mut epochs = constant_combination_epochs();
1125 epochs[1].observations[0].lli1 = Some(1);
1126
1127 let flags = detect_cycle_slips(&epochs, quiet_cycle_slip_config()).expect("LLI flags");
1128 let slipped = slipped_epoch_satellites(&flags);
1129
1130 assert_eq!(slipped, vec![(1, "G01".to_string(), vec![SlipReason::Lli])]);
1131 }
1132
1133 #[test]
1134 fn cycle_slip_lli_epoch_restarts_arc_at_marked_epoch() {
1135 let mut state = SatelliteCycleSlipState::new();
1136 let config = quiet_cycle_slip_config();
1137
1138 for epoch in 0..50 {
1139 let flag = classify_dual_frequency_observation(
1140 &mut state,
1141 &combined_observation("G01", epoch, 5.0, 0.0),
1142 Some(epoch as f64 * 30.0),
1143 config,
1144 )
1145 .expect("pre-slip epoch");
1146 assert!(!flag.slip);
1147 }
1148
1149 let mut lli_epoch = combined_observation("G01", 50, 20.0, 0.50);
1150 lli_epoch.lli1 = Some(1);
1151 let lli_flag =
1152 classify_dual_frequency_observation(&mut state, &lli_epoch, Some(50.0 * 30.0), config)
1153 .expect("LLI epoch");
1154
1155 assert_eq!(lli_flag.reasons, vec![SlipReason::Lli]);
1156 assert_eq!(state.previous_epoch_time_s, Some(50.0 * 30.0));
1157 assert_close(state.previous_geometry_free_m.expect("GF state"), 0.50);
1158 assert_close(
1159 state.previous_melbourne_wubbena_cycles.expect("MW state"),
1160 20.0,
1161 );
1162 assert_eq!(state.melbourne_wubbena.sample_count, 1);
1163 assert_close(state.melbourne_wubbena.mean, 20.0);
1164 assert_close(state.melbourne_wubbena.variance, 0.0);
1165
1166 let clean_flag = classify_dual_frequency_observation(
1167 &mut state,
1168 &combined_observation("G01", 51, 20.05, 0.51),
1169 Some(51.0 * 30.0),
1170 config,
1171 )
1172 .expect("post-LLI epoch");
1173
1174 assert!(!clean_flag.slip);
1175 assert_eq!(state.melbourne_wubbena.sample_count, 2);
1176 }
1177
1178 #[test]
1179 fn cycle_slip_unmarked_epoch_keeps_existing_arc_history() {
1180 let mut state = SatelliteCycleSlipState::new();
1181 let config = quiet_cycle_slip_config();
1182
1183 for epoch in 0..3 {
1184 let flag = classify_dual_frequency_observation(
1185 &mut state,
1186 &combined_observation("G01", epoch, 5.0 + epoch as f64 * 0.1, 0.0),
1187 Some(epoch as f64 * 30.0),
1188 config,
1189 )
1190 .expect("clean epoch");
1191 assert!(!flag.slip);
1192 }
1193
1194 assert_eq!(state.previous_epoch_time_s, Some(60.0));
1195 assert_eq!(state.melbourne_wubbena.sample_count, 3);
1196 assert_close(state.melbourne_wubbena.mean, 5.1);
1197 }
1198
1199 #[test]
1200 fn cycle_slip_driver_leaves_clean_lli_unflagged() {
1201 let mut epochs = constant_combination_epochs();
1202 epochs[1].observations[0].lli1 = Some(0);
1203
1204 let flags = detect_cycle_slips(&epochs, quiet_cycle_slip_config()).expect("clean LLI");
1205 let slipped = slipped_epoch_satellites(&flags);
1206
1207 assert!(slipped.is_empty());
1208 }
1209
1210 #[test]
1211 fn cycle_slip_driver_initializes_new_and_reacquired_arcs_without_spurious_slips() {
1212 let epochs = vec![
1213 super::super::prep::DualFrequencyEpoch {
1214 gap_time_s: Some(0.0),
1215 observations: vec![combined_observation("G01", 0, 5.0, 0.00)],
1216 },
1217 super::super::prep::DualFrequencyEpoch {
1218 gap_time_s: Some(30.0),
1219 observations: vec![
1220 combined_observation("G01", 1, 5.0, 0.01),
1221 combined_observation("G02", 1, 20.0, 0.50),
1222 ],
1223 },
1224 super::super::prep::DualFrequencyEpoch {
1225 gap_time_s: Some(600.0),
1226 observations: vec![combined_observation("G01", 2, 20.0, 0.80)],
1227 },
1228 ];
1229 let config = CycleSlipConfig {
1230 melbourne_wubbena_threshold_cycles: 2.0,
1231 geometry_free_threshold_m: 0.05,
1232 maximum_gap_s: 120.0,
1233 ..CycleSlipConfig::default()
1234 };
1235
1236 let flags = detect_cycle_slips(&epochs, config).expect("cycle-slip driver");
1237
1238 assert!(flags
1239 .iter()
1240 .flat_map(|epoch| &epoch.observations)
1241 .all(|observation| !observation.slip));
1242 }
1243
1244 #[test]
1245 fn cycle_slip_driver_carries_time_across_untimed_samples() {
1246 let config = CycleSlipConfig {
1247 melbourne_wubbena_threshold_cycles: 2.0,
1248 geometry_free_threshold_m: 0.05,
1249 maximum_gap_s: 120.0,
1250 ..CycleSlipConfig::default()
1251 };
1252 let intermittent = vec![
1253 super::super::prep::DualFrequencyEpoch {
1254 gap_time_s: Some(0.0),
1255 observations: vec![combined_observation("G01", 0, 5.0, 0.00)],
1256 },
1257 super::super::prep::DualFrequencyEpoch {
1258 gap_time_s: None,
1259 observations: vec![combined_observation("G01", 1, 5.2, 0.01)],
1260 },
1261 super::super::prep::DualFrequencyEpoch {
1262 gap_time_s: Some(200.0),
1263 observations: vec![combined_observation("G01", 2, 11.0, 0.50)],
1264 },
1265 ];
1266 let timed = vec![
1267 super::super::prep::DualFrequencyEpoch {
1268 gap_time_s: Some(0.0),
1269 observations: vec![combined_observation("G01", 0, 5.0, 0.00)],
1270 },
1271 super::super::prep::DualFrequencyEpoch {
1272 gap_time_s: Some(30.0),
1273 observations: vec![combined_observation("G01", 1, 5.2, 0.01)],
1274 },
1275 super::super::prep::DualFrequencyEpoch {
1276 gap_time_s: Some(60.0),
1277 observations: vec![combined_observation("G01", 2, 11.0, 0.50)],
1278 },
1279 ];
1280
1281 let intermittent_flags =
1282 detect_cycle_slips(&intermittent, config).expect("intermittent timestamps");
1283 let timed_flags = detect_cycle_slips(&timed, config).expect("timed timestamps");
1284
1285 assert!(slipped_epoch_satellites(&intermittent_flags).is_empty());
1286 assert_eq!(
1287 slipped_epoch_satellites(&timed_flags),
1288 vec![(
1289 2,
1290 "G01".to_string(),
1291 vec![SlipReason::GeometryFree, SlipReason::MelbourneWubbena]
1292 )]
1293 );
1294 }
1295
1296 #[test]
1297 fn cycle_slip_driver_rejects_unordered_epoch_times() {
1298 let epochs = vec![
1299 super::super::prep::DualFrequencyEpoch {
1300 gap_time_s: Some(30.0),
1301 observations: Vec::new(),
1302 },
1303 super::super::prep::DualFrequencyEpoch {
1304 gap_time_s: Some(0.0),
1305 observations: Vec::new(),
1306 },
1307 ];
1308
1309 assert_eq!(
1310 detect_cycle_slips(&epochs, CycleSlipConfig::default()),
1311 Err(CycleSlipError::EpochsNotOrdered)
1312 );
1313 }
1314
1315 #[test]
1316 fn cycle_slip_geometry_free_rejects_nonfinite_phase_meter_output() {
1317 let mut observation = gf_observation("G01", 0, 0.25);
1318 observation.f1_hz = f64::MIN_POSITIVE;
1319
1320 assert_eq!(
1321 geometry_free_m(&observation),
1322 Err(CycleSlipError::NonFiniteObservation)
1323 );
1324 }
1325
1326 fn mw_observation(
1327 satellite_id: &str,
1328 epoch_index: usize,
1329 wide_lane_cycles: f64,
1330 ) -> DualFrequencyObservation {
1331 let geometric_m = 23_000_000.0 + epoch_index as f64 * 100.0;
1332 let n2 = 80_000.0;
1333 let n1 = n2 + wide_lane_cycles;
1334 let lambda1 = C_M_S / F_L1_HZ;
1335 let lambda2 = C_M_S / F_L2_HZ;
1336
1337 DualFrequencyObservation {
1338 satellite_id: satellite_id.to_string(),
1339 ambiguity_id: satellite_id.to_string(),
1340 p1_m: geometric_m,
1341 p2_m: geometric_m,
1342 phi1_cyc: (geometric_m + n1 * lambda1) / lambda1,
1343 phi2_cyc: (geometric_m + n2 * lambda2) / lambda2,
1344 f1_hz: F_L1_HZ,
1345 f2_hz: F_L2_HZ,
1346 lli1: None,
1347 lli2: None,
1348 }
1349 }
1350
1351 fn gf_observation(
1352 satellite_id: &str,
1353 epoch_index: usize,
1354 geometry_free_m: f64,
1355 ) -> DualFrequencyObservation {
1356 let geometric_m = 23_000_000.0 + epoch_index as f64 * 100.0;
1357 let lambda1 = C_M_S / F_L1_HZ;
1358 let lambda2 = C_M_S / F_L2_HZ;
1359
1360 DualFrequencyObservation {
1361 satellite_id: satellite_id.to_string(),
1362 ambiguity_id: satellite_id.to_string(),
1363 p1_m: geometric_m,
1364 p2_m: geometric_m,
1365 phi1_cyc: geometric_m / lambda1,
1366 phi2_cyc: (geometric_m - geometry_free_m) / lambda2,
1367 f1_hz: F_L1_HZ,
1368 f2_hz: F_L2_HZ,
1369 lli1: None,
1370 lli2: None,
1371 }
1372 }
1373
1374 fn combined_observation(
1375 satellite_id: &str,
1376 epoch_index: usize,
1377 melbourne_wubbena_cycles: f64,
1378 geometry_free_m: f64,
1379 ) -> DualFrequencyObservation {
1380 let geometric_m = 23_000_000.0 + epoch_index as f64 * 100.0;
1381 let lambda1 = C_M_S / F_L1_HZ;
1382 let lambda2 = C_M_S / F_L2_HZ;
1383 let lambda_wl = C_M_S / (F_L1_HZ - F_L2_HZ);
1384 let l2_m = geometric_m + lambda_wl * (melbourne_wubbena_cycles - geometry_free_m / lambda1);
1385 let l1_m = l2_m + geometry_free_m;
1386
1387 DualFrequencyObservation {
1388 satellite_id: satellite_id.to_string(),
1389 ambiguity_id: satellite_id.to_string(),
1390 p1_m: geometric_m,
1391 p2_m: geometric_m,
1392 phi1_cyc: l1_m / lambda1,
1393 phi2_cyc: l2_m / lambda2,
1394 f1_hz: F_L1_HZ,
1395 f2_hz: F_L2_HZ,
1396 lli1: None,
1397 lli2: None,
1398 }
1399 }
1400
1401 fn combined_observation_with_ambiguity(
1402 satellite_id: &str,
1403 ambiguity_id: &str,
1404 epoch_index: usize,
1405 melbourne_wubbena_cycles: f64,
1406 geometry_free_m: f64,
1407 ) -> DualFrequencyObservation {
1408 let mut observation = combined_observation(
1409 satellite_id,
1410 epoch_index,
1411 melbourne_wubbena_cycles,
1412 geometry_free_m,
1413 );
1414 observation.ambiguity_id = ambiguity_id.to_string();
1415 observation
1416 }
1417
1418 fn constant_combination_epochs() -> Vec<super::super::prep::DualFrequencyEpoch> {
1419 (0..3)
1420 .map(|epoch| super::super::prep::DualFrequencyEpoch {
1421 gap_time_s: Some(epoch as f64 * 30.0),
1422 observations: vec![combined_observation("G01", epoch, 5.0, 0.0)],
1423 })
1424 .collect()
1425 }
1426
1427 fn quiet_cycle_slip_config() -> CycleSlipConfig {
1428 CycleSlipConfig {
1429 melbourne_wubbena_threshold_cycles: 2.0,
1430 geometry_free_threshold_m: 0.05,
1431 maximum_gap_s: 120.0,
1432 ..CycleSlipConfig::default()
1433 }
1434 }
1435
1436 fn slipped_epoch_satellites(
1437 flags: &[CycleSlipFlagEpoch],
1438 ) -> Vec<(usize, String, Vec<SlipReason>)> {
1439 flags
1440 .iter()
1441 .enumerate()
1442 .flat_map(|(epoch_index, epoch)| {
1443 epoch
1444 .observations
1445 .iter()
1446 .filter(|observation| observation.slip)
1447 .map(move |observation| {
1448 (
1449 epoch_index,
1450 observation.satellite_id.clone(),
1451 observation.reasons.clone(),
1452 )
1453 })
1454 })
1455 .collect()
1456 }
1457
1458 fn slipped_epoch_observations(
1459 flags: &[CycleSlipFlagEpoch],
1460 ) -> Vec<(usize, usize, String, Vec<SlipReason>)> {
1461 flags
1462 .iter()
1463 .enumerate()
1464 .flat_map(|(epoch_index, epoch)| {
1465 epoch
1466 .observations
1467 .iter()
1468 .enumerate()
1469 .filter(|(_, observation)| observation.slip)
1470 .map(move |(observation_index, observation)| {
1471 (
1472 epoch_index,
1473 observation_index,
1474 observation.satellite_id.clone(),
1475 observation.reasons.clone(),
1476 )
1477 })
1478 })
1479 .collect()
1480 }
1481
1482 fn assert_close(actual: f64, expected: f64) {
1483 assert!(
1484 (actual - expected).abs() < 1.0e-6,
1485 "expected {actual} to be within tolerance of {expected}"
1486 );
1487 }
1488}