Skip to main content

sidereon_core/clock_stability/
mod.rs

1//! Clock-stability estimators from IEEE Std 1139-2008 and NIST SP 1065.
2//!
3//! The natural RINEX receiver-clock phase input is
4//! `crates/sidereon-core/src/rinex_obs/mod.rs:ObsEpoch::rcv_clock_offset_s`.
5//! [`receiver_clock_phase_deviations`] exposes that field as a phase-deviation
6//! series in seconds.
7//!
8//! Gap handling is explicit. [`GapPolicy::Reject`] requires a contiguous series.
9//! [`GapPolicy::OmitTerms`] keeps the series indexed and excludes every estimator
10//! term whose required phase samples cross a missing sample.
11
12use crate::rinex::observations::RinexObs;
13
14const SQRT_3: f64 = 1.732_050_807_568_877_2;
15
16/// Tagged input samples for Allan-family estimators.
17#[derive(Debug, Clone, Copy, PartialEq)]
18pub enum AllanSeries<'a> {
19    /// Phase deviations in seconds, sampled every `tau0_s`.
20    PhaseSeconds(&'a [f64]),
21    /// Fractional-frequency samples, dimensionless, sampled every `tau0_s`.
22    FractionalFrequency(&'a [f64]),
23    /// Phase deviations in seconds with missing samples.
24    PhaseSecondsWithGaps(&'a [Option<f64>]),
25    /// Fractional-frequency samples with missing samples.
26    FractionalFrequencyWithGaps(&'a [Option<f64>]),
27}
28
29/// Averaging-factor grid for requested estimator points.
30#[derive(Debug, Clone, PartialEq, Eq, Default)]
31pub enum TauGrid {
32    /// `m = 1, 2, 4, 8, ...` while the estimator has at least one term.
33    #[default]
34    Octave,
35    /// `m = 1..=m_max` while the estimator has at least one term.
36    All,
37    /// Caller-supplied averaging factors.
38    Explicit(Vec<usize>),
39}
40
41/// Missing-sample policy.
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
43pub enum GapPolicy {
44    /// Reject any missing sample before estimation.
45    #[default]
46    Reject,
47    /// Exclude estimator terms whose phase samples cross a missing sample.
48    OmitTerms,
49}
50
51/// Estimators available through [`compute_allan_deviations`].
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub struct AllanEstimatorSet {
54    /// Plain non-overlapping Allan deviation.
55    pub adev: bool,
56    /// Fully overlapping Allan deviation.
57    pub overlapping_adev: bool,
58    /// Modified Allan deviation.
59    pub mdev: bool,
60    /// Overlapping Hadamard deviation.
61    pub hdev: bool,
62    /// Time deviation derived from MDEV.
63    pub tdev: bool,
64}
65
66impl AllanEstimatorSet {
67    /// No estimators selected.
68    pub const fn none() -> Self {
69        Self {
70            adev: false,
71            overlapping_adev: false,
72            mdev: false,
73            hdev: false,
74            tdev: false,
75        }
76    }
77
78    /// Standard overlapping estimators plus TDEV.
79    pub const fn standard() -> Self {
80        Self {
81            adev: false,
82            overlapping_adev: true,
83            mdev: true,
84            hdev: true,
85            tdev: true,
86        }
87    }
88
89    /// Every implemented estimator.
90    pub const fn all() -> Self {
91        Self {
92            adev: true,
93            overlapping_adev: true,
94            mdev: true,
95            hdev: true,
96            tdev: true,
97        }
98    }
99
100    fn is_empty(self) -> bool {
101        !self.adev && !self.overlapping_adev && !self.mdev && !self.hdev && !self.tdev
102    }
103}
104
105impl Default for AllanEstimatorSet {
106    fn default() -> Self {
107        Self::standard()
108    }
109}
110
111/// Options for the combined Allan-family driver.
112#[derive(Debug, Clone, PartialEq, Eq, Default)]
113pub struct AllanOptions {
114    /// Which estimators to compute.
115    pub estimators: AllanEstimatorSet,
116    /// Averaging-factor grid.
117    pub tau_grid: TauGrid,
118    /// Missing-sample policy.
119    pub gap_policy: GapPolicy,
120}
121
122/// Input package for [`compute_allan_deviations`].
123#[derive(Debug, Clone, PartialEq)]
124pub struct AllanInput<'a> {
125    /// Tagged sample series.
126    pub series: AllanSeries<'a>,
127    /// Basic sampling interval in seconds.
128    pub tau0_s: f64,
129    /// Estimator, tau-grid, and gap options.
130    pub options: AllanOptions,
131}
132
133/// One estimator curve.
134#[derive(Debug, Clone, PartialEq)]
135pub struct AllanResult {
136    /// Averaging times, seconds.
137    pub tau_s: Vec<f64>,
138    /// Deviation value for each averaging time.
139    pub deviation: Vec<f64>,
140    /// Number of estimator terms used at each averaging time.
141    pub n: Vec<usize>,
142}
143
144impl AllanResult {
145    fn new() -> Self {
146        Self {
147            tau_s: Vec::new(),
148            deviation: Vec::new(),
149            n: Vec::new(),
150        }
151    }
152
153    fn push(&mut self, tau_s: f64, deviation: f64, n: usize) {
154        self.tau_s.push(tau_s);
155        self.deviation.push(deviation);
156        self.n.push(n);
157    }
158
159    /// Number of tau points in the curve.
160    pub fn len(&self) -> usize {
161        self.tau_s.len()
162    }
163
164    /// Whether the curve has no tau points.
165    pub fn is_empty(&self) -> bool {
166        self.tau_s.is_empty()
167    }
168}
169
170/// Combined output from [`compute_allan_deviations`].
171#[derive(Debug, Clone, PartialEq, Default)]
172pub struct AllanDeviationCurves {
173    /// Plain non-overlapping Allan deviation, if requested.
174    pub adev: Option<AllanResult>,
175    /// Fully overlapping Allan deviation, if requested.
176    pub overlapping_adev: Option<AllanResult>,
177    /// Modified Allan deviation, if requested.
178    pub mdev: Option<AllanResult>,
179    /// Overlapping Hadamard deviation, if requested.
180    pub hdev: Option<AllanResult>,
181    /// Time deviation, if requested.
182    pub tdev: Option<AllanResult>,
183}
184
185/// Estimator identifier used in errors and options.
186#[derive(Debug, Clone, Copy, PartialEq, Eq)]
187pub enum AllanEstimator {
188    /// Plain non-overlapping Allan deviation.
189    Adev,
190    /// Fully overlapping Allan deviation.
191    OverlappingAdev,
192    /// Modified Allan deviation.
193    Mdev,
194    /// Overlapping Hadamard deviation.
195    Hdev,
196    /// Time deviation.
197    Tdev,
198}
199
200impl AllanEstimator {
201    fn label(self) -> &'static str {
202        match self {
203            Self::Adev => "ADEV",
204            Self::OverlappingAdev => "OADEV",
205            Self::Mdev => "MDEV",
206            Self::Hdev => "HDEV",
207            Self::Tdev => "TDEV",
208        }
209    }
210}
211
212/// Error from Allan-family estimator setup or evaluation.
213#[derive(Debug, Clone, PartialEq)]
214pub enum AllanError {
215    /// The input series has no samples.
216    EmptySeries,
217    /// The basic sampling interval is not finite and positive.
218    InvalidTau0 { tau0_s: f64 },
219    /// No estimator was requested.
220    NoEstimators,
221    /// An explicit tau grid was empty.
222    EmptyTauGrid,
223    /// Averaging factors must be positive.
224    InvalidAveragingFactor { averaging_factor: usize },
225    /// A requested estimator has no valid term for this sample count.
226    TooFewSamples {
227        estimator: AllanEstimator,
228        averaging_factor: usize,
229        available_phase_samples: usize,
230    },
231    /// A sample was not finite.
232    NonFiniteSample { index: usize },
233    /// A missing sample was present under [`GapPolicy::Reject`].
234    Gap { index: usize },
235    /// The computed tau was not finite.
236    NonFiniteTau {
237        estimator: AllanEstimator,
238        averaging_factor: usize,
239    },
240    /// The computed deviation was not finite.
241    NonFiniteDeviation {
242        estimator: AllanEstimator,
243        averaging_factor: usize,
244    },
245}
246
247impl core::fmt::Display for AllanError {
248    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
249        match self {
250            Self::EmptySeries => write!(f, "Allan series is empty"),
251            Self::InvalidTau0 { tau0_s } => {
252                write!(f, "tau0_s must be finite and positive, got {tau0_s}")
253            }
254            Self::NoEstimators => write!(f, "no Allan estimators selected"),
255            Self::EmptyTauGrid => write!(f, "explicit tau grid is empty"),
256            Self::InvalidAveragingFactor { averaging_factor } => {
257                write!(
258                    f,
259                    "averaging factor must be positive, got {averaging_factor}"
260                )
261            }
262            Self::TooFewSamples {
263                estimator,
264                averaging_factor,
265                available_phase_samples,
266            } => write!(
267                f,
268                "{} has no valid terms for averaging factor {} with {} phase samples",
269                estimator.label(),
270                averaging_factor,
271                available_phase_samples
272            ),
273            Self::NonFiniteSample { index } => {
274                write!(f, "sample {index} is not finite")
275            }
276            Self::Gap { index } => {
277                write!(f, "sample {index} is missing")
278            }
279            Self::NonFiniteTau {
280                estimator,
281                averaging_factor,
282            } => write!(
283                f,
284                "{} tau is not finite for averaging factor {}",
285                estimator.label(),
286                averaging_factor
287            ),
288            Self::NonFiniteDeviation {
289                estimator,
290                averaging_factor,
291            } => write!(
292                f,
293                "{} deviation is not finite for averaging factor {}",
294                estimator.label(),
295                averaging_factor
296            ),
297        }
298    }
299}
300
301impl std::error::Error for AllanError {}
302
303/// Extract RINEX receiver-clock offsets as phase deviations in seconds.
304///
305/// Event epochs (`flag > 1`) are returned as gaps. The source field is
306/// `crates/sidereon-core/src/rinex_obs/mod.rs:ObsEpoch::rcv_clock_offset_s`.
307pub fn receiver_clock_phase_deviations(obs: &RinexObs) -> Vec<Option<f64>> {
308    obs.epochs()
309        .iter()
310        .map(|epoch| {
311            if epoch.flag > 1 {
312                None
313            } else {
314                epoch.rcv_clock_offset_s
315            }
316        })
317        .collect()
318}
319
320/// Compute the requested Allan-family curves.
321pub fn compute_allan_deviations(
322    input: &AllanInput<'_>,
323) -> Result<AllanDeviationCurves, AllanError> {
324    if input.options.estimators.is_empty() {
325        return Err(AllanError::NoEstimators);
326    }
327
328    let phase = prepare_phase(input.series, input.tau0_s, input.options.gap_policy)?;
329    let mut curves = AllanDeviationCurves::default();
330
331    if input.options.estimators.adev {
332        curves.adev = Some(compute_curve(
333            &phase,
334            input.tau0_s,
335            &input.options.tau_grid,
336            AllanEstimator::Adev,
337        )?);
338    }
339    if input.options.estimators.overlapping_adev {
340        curves.overlapping_adev = Some(compute_curve(
341            &phase,
342            input.tau0_s,
343            &input.options.tau_grid,
344            AllanEstimator::OverlappingAdev,
345        )?);
346    }
347    if input.options.estimators.mdev {
348        curves.mdev = Some(compute_curve(
349            &phase,
350            input.tau0_s,
351            &input.options.tau_grid,
352            AllanEstimator::Mdev,
353        )?);
354    }
355    if input.options.estimators.hdev {
356        curves.hdev = Some(compute_curve(
357            &phase,
358            input.tau0_s,
359            &input.options.tau_grid,
360            AllanEstimator::Hdev,
361        )?);
362    }
363    if input.options.estimators.tdev {
364        curves.tdev = Some(compute_curve(
365            &phase,
366            input.tau0_s,
367            &input.options.tau_grid,
368            AllanEstimator::Tdev,
369        )?);
370    }
371
372    Ok(curves)
373}
374
375/// Plain non-overlapping Allan deviation for explicit averaging factors.
376pub fn allan_deviation(
377    series: AllanSeries<'_>,
378    tau0_s: f64,
379    averaging_factors: &[usize],
380) -> Result<AllanResult, AllanError> {
381    compute_explicit(series, tau0_s, averaging_factors, AllanEstimator::Adev)
382}
383
384/// Fully overlapping Allan deviation for explicit averaging factors.
385pub fn overlapping_adev(
386    series: AllanSeries<'_>,
387    tau0_s: f64,
388    averaging_factors: &[usize],
389) -> Result<AllanResult, AllanError> {
390    compute_explicit(
391        series,
392        tau0_s,
393        averaging_factors,
394        AllanEstimator::OverlappingAdev,
395    )
396}
397
398/// Modified Allan deviation for explicit averaging factors.
399pub fn modified_adev(
400    series: AllanSeries<'_>,
401    tau0_s: f64,
402    averaging_factors: &[usize],
403) -> Result<AllanResult, AllanError> {
404    compute_explicit(series, tau0_s, averaging_factors, AllanEstimator::Mdev)
405}
406
407/// Overlapping Hadamard deviation for explicit averaging factors.
408pub fn hadamard_deviation(
409    series: AllanSeries<'_>,
410    tau0_s: f64,
411    averaging_factors: &[usize],
412) -> Result<AllanResult, AllanError> {
413    compute_explicit(series, tau0_s, averaging_factors, AllanEstimator::Hdev)
414}
415
416/// Time deviation for explicit averaging factors.
417pub fn time_deviation(
418    series: AllanSeries<'_>,
419    tau0_s: f64,
420    averaging_factors: &[usize],
421) -> Result<AllanResult, AllanError> {
422    compute_explicit(series, tau0_s, averaging_factors, AllanEstimator::Tdev)
423}
424
425fn compute_explicit(
426    series: AllanSeries<'_>,
427    tau0_s: f64,
428    averaging_factors: &[usize],
429    estimator: AllanEstimator,
430) -> Result<AllanResult, AllanError> {
431    let phase = prepare_phase(series, tau0_s, GapPolicy::Reject)?;
432    compute_curve(
433        &phase,
434        tau0_s,
435        &TauGrid::Explicit(averaging_factors.to_vec()),
436        estimator,
437    )
438}
439
440#[derive(Debug, Clone, Copy)]
441struct PhasePoint {
442    value_s: f64,
443    run_id: usize,
444}
445
446fn prepare_phase(
447    series: AllanSeries<'_>,
448    tau0_s: f64,
449    gap_policy: GapPolicy,
450) -> Result<Vec<Option<PhasePoint>>, AllanError> {
451    validate_tau0(tau0_s)?;
452    match series {
453        AllanSeries::PhaseSeconds(values) => phase_from_contiguous(values),
454        AllanSeries::FractionalFrequency(values) => phase_from_contiguous_frequency(values, tau0_s),
455        AllanSeries::PhaseSecondsWithGaps(values) => phase_from_gapped(values, gap_policy),
456        AllanSeries::FractionalFrequencyWithGaps(values) => {
457            phase_from_gapped_frequency(values, tau0_s, gap_policy)
458        }
459    }
460}
461
462fn validate_tau0(tau0_s: f64) -> Result<(), AllanError> {
463    if tau0_s.is_finite() && tau0_s > 0.0 {
464        Ok(())
465    } else {
466        Err(AllanError::InvalidTau0 { tau0_s })
467    }
468}
469
470fn phase_from_contiguous(values: &[f64]) -> Result<Vec<Option<PhasePoint>>, AllanError> {
471    if values.is_empty() {
472        return Err(AllanError::EmptySeries);
473    }
474    values
475        .iter()
476        .enumerate()
477        .map(|(index, &value_s)| {
478            validate_sample(index, value_s).map(|value_s| Some(PhasePoint { value_s, run_id: 0 }))
479        })
480        .collect()
481}
482
483fn phase_from_contiguous_frequency(
484    values: &[f64],
485    tau0_s: f64,
486) -> Result<Vec<Option<PhasePoint>>, AllanError> {
487    if values.is_empty() {
488        return Err(AllanError::EmptySeries);
489    }
490    let mut phase = Vec::with_capacity(values.len() + 1);
491    let mut value_s = 0.0;
492    phase.push(Some(PhasePoint { value_s, run_id: 0 }));
493    for (index, &frequency) in values.iter().enumerate() {
494        let frequency = validate_sample(index, frequency)?;
495        value_s += tau0_s * frequency;
496        if !value_s.is_finite() {
497            return Err(AllanError::NonFiniteSample { index });
498        }
499        phase.push(Some(PhasePoint { value_s, run_id: 0 }));
500    }
501    Ok(phase)
502}
503
504fn phase_from_gapped(
505    values: &[Option<f64>],
506    gap_policy: GapPolicy,
507) -> Result<Vec<Option<PhasePoint>>, AllanError> {
508    if values.is_empty() {
509        return Err(AllanError::EmptySeries);
510    }
511
512    let mut phase = Vec::with_capacity(values.len());
513    let mut run_id = 0usize;
514    let mut in_run = false;
515    for (index, value) in values.iter().enumerate() {
516        match value {
517            Some(value_s) => {
518                let value_s = validate_sample(index, *value_s)?;
519                if !in_run {
520                    run_id += 1;
521                    in_run = true;
522                }
523                phase.push(Some(PhasePoint { value_s, run_id }));
524            }
525            None => {
526                if gap_policy == GapPolicy::Reject {
527                    return Err(AllanError::Gap { index });
528                }
529                in_run = false;
530                phase.push(None);
531            }
532        }
533    }
534    Ok(phase)
535}
536
537fn phase_from_gapped_frequency(
538    values: &[Option<f64>],
539    tau0_s: f64,
540    gap_policy: GapPolicy,
541) -> Result<Vec<Option<PhasePoint>>, AllanError> {
542    if values.is_empty() {
543        return Err(AllanError::EmptySeries);
544    }
545    if gap_policy == GapPolicy::Reject {
546        for (index, value) in values.iter().enumerate() {
547            if value.is_none() {
548                return Err(AllanError::Gap { index });
549            }
550        }
551    }
552
553    let mut phase = vec![None; values.len() + 1];
554    let mut run_id = 0usize;
555    let mut index = 0usize;
556    while index < values.len() {
557        if values[index].is_none() {
558            index += 1;
559            continue;
560        };
561
562        run_id += 1;
563        let mut value_s = 0.0;
564        phase[index] = Some(PhasePoint { value_s, run_id });
565        let mut sample_index = index;
566        while let Some(current) = values.get(sample_index).copied().flatten() {
567            let current = validate_sample(sample_index, current)?;
568            value_s += tau0_s * current;
569            if !value_s.is_finite() {
570                return Err(AllanError::NonFiniteSample {
571                    index: sample_index,
572                });
573            }
574            phase[sample_index + 1] = Some(PhasePoint { value_s, run_id });
575            sample_index += 1;
576        }
577        index = sample_index + 1;
578    }
579
580    Ok(phase)
581}
582
583fn validate_sample(index: usize, value: f64) -> Result<f64, AllanError> {
584    if value.is_finite() {
585        Ok(value)
586    } else {
587        Err(AllanError::NonFiniteSample { index })
588    }
589}
590
591fn compute_curve(
592    phase: &[Option<PhasePoint>],
593    tau0_s: f64,
594    tau_grid: &TauGrid,
595    estimator: AllanEstimator,
596) -> Result<AllanResult, AllanError> {
597    let averaging_factors = averaging_factors(phase.len(), estimator, tau_grid)?;
598    let strict = matches!(tau_grid, TauGrid::Explicit(_));
599    let mut result = AllanResult::new();
600
601    for m in averaging_factors {
602        if m == 0 {
603            return Err(AllanError::InvalidAveragingFactor {
604                averaging_factor: m,
605            });
606        }
607        if candidate_count(phase.len(), estimator, m).is_none() {
608            if strict {
609                return Err(AllanError::TooFewSamples {
610                    estimator,
611                    averaging_factor: m,
612                    available_phase_samples: phase.len(),
613                });
614            }
615            continue;
616        }
617
618        let tau_s = tau0_s * m as f64;
619        if !tau_s.is_finite() {
620            return Err(AllanError::NonFiniteTau {
621                estimator,
622                averaging_factor: m,
623            });
624        }
625
626        let (sum_sq, n) = estimator_sum_squares(phase, estimator, m);
627        if n == 0 {
628            if strict {
629                return Err(AllanError::TooFewSamples {
630                    estimator,
631                    averaging_factor: m,
632                    available_phase_samples: phase.len(),
633                });
634            }
635            continue;
636        }
637
638        let deviation = deviation_from_sum(sum_sq, n, tau_s, m, estimator);
639        if !deviation.is_finite() {
640            return Err(AllanError::NonFiniteDeviation {
641                estimator,
642                averaging_factor: m,
643            });
644        }
645        result.push(tau_s, deviation, n);
646    }
647
648    if result.is_empty() {
649        return Err(AllanError::TooFewSamples {
650            estimator,
651            averaging_factor: 1,
652            available_phase_samples: phase.len(),
653        });
654    }
655    Ok(result)
656}
657
658fn averaging_factors(
659    phase_len: usize,
660    estimator: AllanEstimator,
661    tau_grid: &TauGrid,
662) -> Result<Vec<usize>, AllanError> {
663    match tau_grid {
664        TauGrid::Explicit(values) => {
665            if values.is_empty() {
666                Err(AllanError::EmptyTauGrid)
667            } else {
668                Ok(values.clone())
669            }
670        }
671        TauGrid::All => Ok((1..=max_averaging_factor(phase_len, estimator)).collect()),
672        TauGrid::Octave => {
673            let max_m = max_averaging_factor(phase_len, estimator);
674            let mut values = Vec::new();
675            let mut m = 1usize;
676            while m <= max_m {
677                values.push(m);
678                if m > max_m / 2 {
679                    break;
680                }
681                m *= 2;
682            }
683            Ok(values)
684        }
685    }
686}
687
688fn max_averaging_factor(phase_len: usize, estimator: AllanEstimator) -> usize {
689    match estimator {
690        AllanEstimator::Adev | AllanEstimator::OverlappingAdev => phase_len.saturating_sub(1) / 2,
691        AllanEstimator::Mdev | AllanEstimator::Tdev => phase_len / 3,
692        AllanEstimator::Hdev => phase_len.saturating_sub(1) / 3,
693    }
694}
695
696fn candidate_count(phase_len: usize, estimator: AllanEstimator, m: usize) -> Option<usize> {
697    if m == 0 {
698        return None;
699    }
700    match estimator {
701        AllanEstimator::Adev => {
702            let frequency_len = phase_len.checked_sub(1)?;
703            (frequency_len / m).checked_sub(1)
704        }
705        AllanEstimator::OverlappingAdev => phase_len.checked_sub(m.checked_mul(2)?),
706        AllanEstimator::Mdev | AllanEstimator::Tdev => {
707            phase_len.checked_sub(m.checked_mul(3)?)?.checked_add(1)
708        }
709        AllanEstimator::Hdev => phase_len.checked_sub(m.checked_mul(3)?),
710    }
711}
712
713fn estimator_sum_squares(
714    phase: &[Option<PhasePoint>],
715    estimator: AllanEstimator,
716    m: usize,
717) -> (f64, usize) {
718    match estimator {
719        AllanEstimator::Adev => plain_adev_sum_squares(phase, m),
720        AllanEstimator::OverlappingAdev => overlapping_adev_sum_squares(phase, m),
721        AllanEstimator::Mdev | AllanEstimator::Tdev => modified_adev_sum_squares(phase, m),
722        AllanEstimator::Hdev => hadamard_sum_squares(phase, m),
723    }
724}
725
726fn deviation_from_sum(
727    sum_sq: f64,
728    n: usize,
729    tau_s: f64,
730    m: usize,
731    estimator: AllanEstimator,
732) -> f64 {
733    let n = n as f64;
734    match estimator {
735        AllanEstimator::Adev | AllanEstimator::OverlappingAdev => {
736            (sum_sq / (2.0 * n * tau_s * tau_s)).sqrt()
737        }
738        AllanEstimator::Mdev => {
739            let mf = m as f64;
740            (sum_sq / (2.0 * mf * mf * n * tau_s * tau_s)).sqrt()
741        }
742        AllanEstimator::Hdev => (sum_sq / (6.0 * n * tau_s * tau_s)).sqrt(),
743        AllanEstimator::Tdev => {
744            let mf = m as f64;
745            let mdev = (sum_sq / (2.0 * mf * mf * n * tau_s * tau_s)).sqrt();
746            tau_s * mdev / SQRT_3
747        }
748    }
749}
750
751fn plain_adev_sum_squares(phase: &[Option<PhasePoint>], m: usize) -> (f64, usize) {
752    let Some(count) = candidate_count(phase.len(), AllanEstimator::Adev, m) else {
753        return (0.0, 0);
754    };
755    let mut sum_sq = 0.0;
756    let mut n = 0usize;
757    for j in 0..count {
758        let i = j * m;
759        if let Some((diff, _)) = second_difference(phase, i, m) {
760            let square = diff * diff;
761            sum_sq += square;
762            n += 1;
763        }
764    }
765    (sum_sq, n)
766}
767
768fn overlapping_adev_sum_squares(phase: &[Option<PhasePoint>], m: usize) -> (f64, usize) {
769    let Some(count) = candidate_count(phase.len(), AllanEstimator::OverlappingAdev, m) else {
770        return (0.0, 0);
771    };
772    let mut sum_sq = 0.0;
773    let mut n = 0usize;
774    for i in 0..count {
775        if let Some((diff, _)) = second_difference(phase, i, m) {
776            let square = diff * diff;
777            sum_sq += square;
778            n += 1;
779        }
780    }
781    (sum_sq, n)
782}
783
784fn modified_adev_sum_squares(phase: &[Option<PhasePoint>], m: usize) -> (f64, usize) {
785    let Some(count) = candidate_count(phase.len(), AllanEstimator::Mdev, m) else {
786        return (0.0, 0);
787    };
788    let mut sum_sq = 0.0;
789    let mut n = 0usize;
790    for j in 0..count {
791        let mut inner = 0.0;
792        let mut run_id = None;
793        let mut valid = true;
794        for i in j..(j + m) {
795            let Some((diff, diff_run_id)) = second_difference(phase, i, m) else {
796                valid = false;
797                break;
798            };
799            if run_id.is_some_and(|current| current != diff_run_id) {
800                valid = false;
801                break;
802            }
803            run_id = Some(diff_run_id);
804            inner += diff;
805        }
806        if valid {
807            let square = inner * inner;
808            sum_sq += square;
809            n += 1;
810        }
811    }
812    (sum_sq, n)
813}
814
815fn hadamard_sum_squares(phase: &[Option<PhasePoint>], m: usize) -> (f64, usize) {
816    let Some(count) = candidate_count(phase.len(), AllanEstimator::Hdev, m) else {
817        return (0.0, 0);
818    };
819    let mut sum_sq = 0.0;
820    let mut n = 0usize;
821    for i in 0..count {
822        if let Some(diff) = third_difference(phase, i, m) {
823            let square = diff * diff;
824            sum_sq += square;
825            n += 1;
826        }
827    }
828    (sum_sq, n)
829}
830
831fn second_difference(phase: &[Option<PhasePoint>], i: usize, m: usize) -> Option<(f64, usize)> {
832    let x0 = phase.get(i).copied().flatten()?;
833    let x1 = phase.get(i + m).copied().flatten()?;
834    let x2 = phase.get(i + 2 * m).copied().flatten()?;
835    if x0.run_id != x1.run_id || x0.run_id != x2.run_id {
836        return None;
837    }
838    Some((x2.value_s - 2.0 * x1.value_s + x0.value_s, x0.run_id))
839}
840
841fn third_difference(phase: &[Option<PhasePoint>], i: usize, m: usize) -> Option<f64> {
842    let x0 = phase.get(i).copied().flatten()?;
843    let x1 = phase.get(i + m).copied().flatten()?;
844    let x2 = phase.get(i + 2 * m).copied().flatten()?;
845    let x3 = phase.get(i + 3 * m).copied().flatten()?;
846    if x0.run_id != x1.run_id || x0.run_id != x2.run_id || x0.run_id != x3.run_id {
847        return None;
848    }
849    Some(x3.value_s - 3.0 * x2.value_s + 3.0 * x1.value_s - x0.value_s)
850}