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::estimation::mad_spread;
13use crate::rinex::observations::RinexObs;
14
15const SQRT_3: f64 = 1.732_050_807_568_877_2;
16const DEFAULT_POWER_LAW_MIN_POINTS_PER_OCTAVE: usize = 2;
17const DEFAULT_POWER_LAW_SLOPE_TOLERANCE: f64 = 0.125;
18
19/// Tagged input samples for Allan-family estimators.
20#[derive(Debug, Clone, Copy, PartialEq)]
21pub enum AllanSeries<'a> {
22    /// Phase deviations in seconds, sampled every `tau0_s`.
23    PhaseSeconds(&'a [f64]),
24    /// Fractional-frequency samples, dimensionless, sampled every `tau0_s`.
25    FractionalFrequency(&'a [f64]),
26    /// Phase deviations in seconds with missing samples.
27    PhaseSecondsWithGaps(&'a [Option<f64>]),
28    /// Fractional-frequency samples with missing samples.
29    FractionalFrequencyWithGaps(&'a [Option<f64>]),
30}
31
32/// Averaging-factor grid for requested estimator points.
33#[derive(Debug, Clone, PartialEq, Eq, Default)]
34pub enum TauGrid {
35    /// `m = 1, 2, 4, 8, ...` while the estimator has at least one term.
36    #[default]
37    Octave,
38    /// `m = 1..=m_max` while the estimator has at least one term.
39    All,
40    /// Caller-supplied averaging factors.
41    Explicit(Vec<usize>),
42}
43
44/// Missing-sample policy.
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
46pub enum GapPolicy {
47    /// Reject any missing sample before estimation.
48    #[default]
49    Reject,
50    /// Exclude estimator terms whose phase samples cross a missing sample.
51    OmitTerms,
52}
53
54/// Estimators available through [`compute_allan_deviations`].
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub struct AllanEstimatorSet {
57    /// Plain non-overlapping Allan deviation.
58    pub adev: bool,
59    /// Fully overlapping Allan deviation.
60    pub overlapping_adev: bool,
61    /// Modified Allan deviation.
62    pub mdev: bool,
63    /// Overlapping Hadamard deviation.
64    pub hdev: bool,
65    /// Time deviation derived from MDEV.
66    pub tdev: bool,
67}
68
69impl AllanEstimatorSet {
70    /// No estimators selected.
71    pub const fn none() -> Self {
72        Self {
73            adev: false,
74            overlapping_adev: false,
75            mdev: false,
76            hdev: false,
77            tdev: false,
78        }
79    }
80
81    /// Standard overlapping estimators plus TDEV.
82    pub const fn standard() -> Self {
83        Self {
84            adev: false,
85            overlapping_adev: true,
86            mdev: true,
87            hdev: true,
88            tdev: true,
89        }
90    }
91
92    /// Every implemented estimator.
93    pub const fn all() -> Self {
94        Self {
95            adev: true,
96            overlapping_adev: true,
97            mdev: true,
98            hdev: true,
99            tdev: true,
100        }
101    }
102
103    fn is_empty(self) -> bool {
104        !self.adev && !self.overlapping_adev && !self.mdev && !self.hdev && !self.tdev
105    }
106}
107
108impl Default for AllanEstimatorSet {
109    fn default() -> Self {
110        Self::standard()
111    }
112}
113
114/// Options for the combined Allan-family driver.
115#[derive(Debug, Clone, PartialEq, Eq, Default)]
116pub struct AllanOptions {
117    /// Which estimators to compute.
118    pub estimators: AllanEstimatorSet,
119    /// Averaging-factor grid.
120    pub tau_grid: TauGrid,
121    /// Missing-sample policy.
122    pub gap_policy: GapPolicy,
123}
124
125/// Input package for [`compute_allan_deviations`].
126#[derive(Debug, Clone, PartialEq)]
127pub struct AllanInput<'a> {
128    /// Tagged sample series.
129    pub series: AllanSeries<'a>,
130    /// Basic sampling interval in seconds.
131    pub tau0_s: f64,
132    /// Estimator, tau-grid, and gap options.
133    pub options: AllanOptions,
134}
135
136/// One estimator curve.
137#[derive(Debug, Clone, PartialEq)]
138pub struct AllanResult {
139    /// Averaging times, seconds.
140    pub tau_s: Vec<f64>,
141    /// Deviation value for each averaging time.
142    pub deviation: Vec<f64>,
143    /// Number of estimator terms used at each averaging time.
144    pub n: Vec<usize>,
145}
146
147impl AllanResult {
148    fn new() -> Self {
149        Self {
150            tau_s: Vec::new(),
151            deviation: Vec::new(),
152            n: Vec::new(),
153        }
154    }
155
156    fn push(&mut self, tau_s: f64, deviation: f64, n: usize) {
157        self.tau_s.push(tau_s);
158        self.deviation.push(deviation);
159        self.n.push(n);
160    }
161
162    /// Number of tau points in the curve.
163    pub fn len(&self) -> usize {
164        self.tau_s.len()
165    }
166
167    /// Whether the curve has no tau points.
168    pub fn is_empty(&self) -> bool {
169        self.tau_s.is_empty()
170    }
171}
172
173/// Combined output from [`compute_allan_deviations`].
174#[derive(Debug, Clone, PartialEq, Default)]
175pub struct AllanDeviationCurves {
176    /// Plain non-overlapping Allan deviation, if requested.
177    pub adev: Option<AllanResult>,
178    /// Fully overlapping Allan deviation, if requested.
179    pub overlapping_adev: Option<AllanResult>,
180    /// Modified Allan deviation, if requested.
181    pub mdev: Option<AllanResult>,
182    /// Overlapping Hadamard deviation, if requested.
183    pub hdev: Option<AllanResult>,
184    /// Time deviation, if requested.
185    pub tdev: Option<AllanResult>,
186}
187
188/// IEEE 1139 fractional-frequency PSD power-law noise type.
189#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
190pub enum PowerLawNoiseType {
191    /// Random-walk frequency modulation, `S_y(f) = h_-2 f^-2`.
192    RandomWalkFM,
193    /// Flicker frequency modulation, `S_y(f) = h_-1 f^-1`.
194    FlickerFM,
195    /// White frequency modulation, `S_y(f) = h_0`.
196    WhiteFM,
197    /// Flicker phase modulation, `S_y(f) = h_1 f`.
198    FlickerPM,
199    /// White phase modulation, `S_y(f) = h_2 f^2`.
200    WhitePM,
201}
202
203impl PowerLawNoiseType {
204    /// Spectral exponent `alpha` in `S_y(f) = h_alpha f^alpha`.
205    pub const fn alpha(self) -> i32 {
206        match self {
207            Self::RandomWalkFM => -2,
208            Self::FlickerFM => -1,
209            Self::WhiteFM => 0,
210            Self::FlickerPM => 1,
211            Self::WhitePM => 2,
212        }
213    }
214
215    /// Index into [`PowerLawNoiseFit::coefficients`].
216    pub const fn coefficient_index(self) -> usize {
217        match self {
218            Self::RandomWalkFM => 0,
219            Self::FlickerFM => 1,
220            Self::WhiteFM => 2,
221            Self::FlickerPM => 3,
222            Self::WhitePM => 4,
223        }
224    }
225}
226
227/// Exact rational log-log slope of ADEV versus tau for a power-law type.
228pub fn allan_deviation_power_law_slope(noise_type: PowerLawNoiseType) -> f64 {
229    match noise_type {
230        PowerLawNoiseType::RandomWalkFM => 0.5,
231        PowerLawNoiseType::FlickerFM => 0.0,
232        PowerLawNoiseType::WhiteFM => -0.5,
233        PowerLawNoiseType::FlickerPM | PowerLawNoiseType::WhitePM => -1.0,
234    }
235}
236
237/// Exact rational log-log slope of MDEV versus tau for a power-law type.
238pub fn modified_allan_deviation_power_law_slope(noise_type: PowerLawNoiseType) -> f64 {
239    match noise_type {
240        PowerLawNoiseType::RandomWalkFM => 0.5,
241        PowerLawNoiseType::FlickerFM => 0.0,
242        PowerLawNoiseType::WhiteFM => -0.5,
243        PowerLawNoiseType::FlickerPM => -1.0,
244        PowerLawNoiseType::WhitePM => -1.5,
245    }
246}
247
248/// Exact tau exponent of Allan variance for a power-law type.
249pub fn allan_variance_power_law_tau_exponent(noise_type: PowerLawNoiseType) -> i32 {
250    match noise_type {
251        PowerLawNoiseType::RandomWalkFM => 1,
252        PowerLawNoiseType::FlickerFM => 0,
253        PowerLawNoiseType::WhiteFM => -1,
254        PowerLawNoiseType::FlickerPM | PowerLawNoiseType::WhitePM => -2,
255    }
256}
257
258/// Options for per-octave power-law identification and coefficient fitting.
259#[derive(Debug, Clone, Copy, PartialEq)]
260pub struct PowerLawNoiseOptions {
261    /// Minimum tau points required before an octave can be classified.
262    pub min_points_per_octave: usize,
263    /// Maximum absolute slope error allowed for an exact-rational noise type.
264    pub slope_tolerance: f64,
265    /// Maximum robust local-slope scatter allowed before an octave is ambiguous.
266    pub scatter_tolerance: f64,
267    /// Basic sample interval used by the deviation calculation, seconds.
268    pub basic_tau_s: f64,
269    /// Upper measurement bandwidth, hertz, used by phase-modulation conversions.
270    pub measurement_bandwidth_hz: f64,
271}
272
273impl PowerLawNoiseOptions {
274    /// Construct options with the default octave point count and slope tolerances.
275    pub const fn new(basic_tau_s: f64, measurement_bandwidth_hz: f64) -> Self {
276        Self {
277            min_points_per_octave: DEFAULT_POWER_LAW_MIN_POINTS_PER_OCTAVE,
278            slope_tolerance: DEFAULT_POWER_LAW_SLOPE_TOLERANCE,
279            scatter_tolerance: DEFAULT_POWER_LAW_SLOPE_TOLERANCE,
280            basic_tau_s,
281            measurement_bandwidth_hz,
282        }
283    }
284
285    /// Construct options using the Nyquist bandwidth for samples spaced by `basic_tau_s`.
286    pub fn sampled_at_nyquist(basic_tau_s: f64) -> Self {
287        Self::new(basic_tau_s, 0.5 / basic_tau_s)
288    }
289}
290
291/// Why a tau octave could not receive a dominant noise type.
292#[derive(Debug, Clone, Copy, PartialEq, Eq)]
293pub enum PowerLawOctaveFlag {
294    /// Fewer than [`PowerLawNoiseOptions::min_points_per_octave`] tau points were present.
295    UnderSampled,
296    /// A zero deviation made the log-log slope undefined.
297    DegenerateDeviation,
298    /// MDEV did not have enough tau points to separate phase-modulation types.
299    MissingModifiedAllan,
300}
301
302/// Dominant noise decision for one tau octave.
303#[derive(Debug, Clone, Copy, PartialEq, Eq)]
304pub enum PowerLawOctaveDominance {
305    /// One power-law type was identified.
306    Dominant(PowerLawNoiseType),
307    /// The octave contains conflicting or off-table slopes.
308    Ambiguous,
309    /// The octave is not classifiable because required data are absent.
310    Flagged(PowerLawOctaveFlag),
311}
312
313/// Per-octave power-law classification from local ADEV and MDEV slopes.
314#[derive(Debug, Clone, PartialEq)]
315pub struct PowerLawOctave {
316    /// First tau in the octave, seconds.
317    pub tau_start_s: f64,
318    /// Last tau used in the octave, seconds.
319    pub tau_end_s: f64,
320    /// Number of ADEV tau points used for the octave slope.
321    pub point_count: usize,
322    /// Fitted ADEV log-log slope for this octave, if available.
323    pub adev_slope: Option<f64>,
324    /// Fitted MDEV log-log slope for this octave, if available.
325    pub mdev_slope: Option<f64>,
326    /// Robust scatter of adjacent ADEV slopes inside the octave, if available.
327    pub slope_scatter: Option<f64>,
328    /// Dominant type, ambiguity, or faithful-bound flag for the octave.
329    pub dominance: PowerLawOctaveDominance,
330}
331
332/// Consecutive tau span that supports one fitted power-law coefficient.
333#[derive(Debug, Clone, PartialEq)]
334pub struct PowerLawNoiseRegion {
335    /// Noise type identified across the region.
336    pub noise_type: PowerLawNoiseType,
337    /// First tau in the region, seconds.
338    pub tau_start_s: f64,
339    /// Last tau in the region, seconds.
340    pub tau_end_s: f64,
341    /// Number of classified octaves merged into this region.
342    pub octave_count: usize,
343    /// Number of deviation points used in the coefficient fit.
344    pub point_count: usize,
345    /// Mean local slope from the statistic used for classification.
346    pub mean_slope: f64,
347    /// Fitted PSD coefficient for this region.
348    pub coefficient: f64,
349}
350
351/// IEEE 1139 power-law noise identification and coefficient fit.
352#[derive(Debug, Clone, PartialEq)]
353pub struct PowerLawNoiseFit {
354    /// Dominant classification for each tau octave.
355    pub dominant_per_octave: Vec<PowerLawOctave>,
356    /// PSD coefficients `[h_-2, h_-1, h_0, h_1, h_2]`.
357    ///
358    /// Entries are `NaN` when no faithful identified region supports that coefficient.
359    pub coefficients: [f64; 5],
360    /// Consecutive identified tau regions used by the coefficient fit.
361    pub regions: Vec<PowerLawNoiseRegion>,
362}
363
364/// Error from power-law clock-noise identification setup.
365#[derive(Debug, Clone, PartialEq, Eq)]
366pub enum PowerLawNoiseError {
367    /// An option field is outside its accepted range.
368    InvalidOptions {
369        /// Option field name.
370        field: &'static str,
371        /// Reason the field is invalid.
372        reason: &'static str,
373    },
374    /// A supplied deviation curve is structurally invalid.
375    InvalidCurve {
376        /// Curve label.
377        curve: &'static str,
378        /// Reason the curve is invalid.
379        reason: &'static str,
380    },
381}
382
383impl core::fmt::Display for PowerLawNoiseError {
384    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
385        match self {
386            Self::InvalidOptions { field, reason } => {
387                write!(f, "invalid power-law option {field}: {reason}")
388            }
389            Self::InvalidCurve { curve, reason } => {
390                write!(f, "invalid {curve} curve: {reason}")
391            }
392        }
393    }
394}
395
396impl std::error::Error for PowerLawNoiseError {}
397
398/// Estimator identifier used in errors and options.
399#[derive(Debug, Clone, Copy, PartialEq, Eq)]
400pub enum AllanEstimator {
401    /// Plain non-overlapping Allan deviation.
402    Adev,
403    /// Fully overlapping Allan deviation.
404    OverlappingAdev,
405    /// Modified Allan deviation.
406    Mdev,
407    /// Overlapping Hadamard deviation.
408    Hdev,
409    /// Time deviation.
410    Tdev,
411}
412
413impl AllanEstimator {
414    fn label(self) -> &'static str {
415        match self {
416            Self::Adev => "ADEV",
417            Self::OverlappingAdev => "OADEV",
418            Self::Mdev => "MDEV",
419            Self::Hdev => "HDEV",
420            Self::Tdev => "TDEV",
421        }
422    }
423}
424
425/// Error from Allan-family estimator setup or evaluation.
426#[derive(Debug, Clone, PartialEq)]
427pub enum AllanError {
428    /// The input series has no samples.
429    EmptySeries,
430    /// The basic sampling interval is not finite and positive.
431    InvalidTau0 { tau0_s: f64 },
432    /// No estimator was requested.
433    NoEstimators,
434    /// An explicit tau grid was empty.
435    EmptyTauGrid,
436    /// Averaging factors must be positive.
437    InvalidAveragingFactor { averaging_factor: usize },
438    /// A requested estimator has no valid term for this sample count.
439    TooFewSamples {
440        estimator: AllanEstimator,
441        averaging_factor: usize,
442        available_phase_samples: usize,
443    },
444    /// A sample was not finite.
445    NonFiniteSample { index: usize },
446    /// A missing sample was present under [`GapPolicy::Reject`].
447    Gap { index: usize },
448    /// The computed tau was not finite.
449    NonFiniteTau {
450        estimator: AllanEstimator,
451        averaging_factor: usize,
452    },
453    /// The computed deviation was not finite.
454    NonFiniteDeviation {
455        estimator: AllanEstimator,
456        averaging_factor: usize,
457    },
458}
459
460impl core::fmt::Display for AllanError {
461    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
462        match self {
463            Self::EmptySeries => write!(f, "Allan series is empty"),
464            Self::InvalidTau0 { tau0_s } => {
465                write!(f, "tau0_s must be finite and positive, got {tau0_s}")
466            }
467            Self::NoEstimators => write!(f, "no Allan estimators selected"),
468            Self::EmptyTauGrid => write!(f, "explicit tau grid is empty"),
469            Self::InvalidAveragingFactor { averaging_factor } => {
470                write!(
471                    f,
472                    "averaging factor must be positive, got {averaging_factor}"
473                )
474            }
475            Self::TooFewSamples {
476                estimator,
477                averaging_factor,
478                available_phase_samples,
479            } => write!(
480                f,
481                "{} has no valid terms for averaging factor {} with {} phase samples",
482                estimator.label(),
483                averaging_factor,
484                available_phase_samples
485            ),
486            Self::NonFiniteSample { index } => {
487                write!(f, "sample {index} is not finite")
488            }
489            Self::Gap { index } => {
490                write!(f, "sample {index} is missing")
491            }
492            Self::NonFiniteTau {
493                estimator,
494                averaging_factor,
495            } => write!(
496                f,
497                "{} tau is not finite for averaging factor {}",
498                estimator.label(),
499                averaging_factor
500            ),
501            Self::NonFiniteDeviation {
502                estimator,
503                averaging_factor,
504            } => write!(
505                f,
506                "{} deviation is not finite for averaging factor {}",
507                estimator.label(),
508                averaging_factor
509            ),
510        }
511    }
512}
513
514impl std::error::Error for AllanError {}
515
516/// Extract RINEX receiver-clock offsets as phase deviations in seconds.
517///
518/// Event epochs (`flag > 1`) are returned as gaps. The source field is
519/// `crates/sidereon-core/src/rinex_obs/mod.rs:ObsEpoch::rcv_clock_offset_s`.
520pub fn receiver_clock_phase_deviations(obs: &RinexObs) -> Vec<Option<f64>> {
521    obs.epochs()
522        .iter()
523        .map(|epoch| {
524            if epoch.flag > 1 {
525                None
526            } else {
527                epoch.rcv_clock_offset_s
528            }
529        })
530        .collect()
531}
532
533/// Compute the requested Allan-family curves.
534pub fn compute_allan_deviations(
535    input: &AllanInput<'_>,
536) -> Result<AllanDeviationCurves, AllanError> {
537    if input.options.estimators.is_empty() {
538        return Err(AllanError::NoEstimators);
539    }
540
541    let phase = prepare_phase(input.series, input.tau0_s, input.options.gap_policy)?;
542    let mut curves = AllanDeviationCurves::default();
543
544    if input.options.estimators.adev {
545        curves.adev = Some(compute_curve(
546            &phase,
547            input.tau0_s,
548            &input.options.tau_grid,
549            AllanEstimator::Adev,
550        )?);
551    }
552    if input.options.estimators.overlapping_adev {
553        curves.overlapping_adev = Some(compute_curve(
554            &phase,
555            input.tau0_s,
556            &input.options.tau_grid,
557            AllanEstimator::OverlappingAdev,
558        )?);
559    }
560    if input.options.estimators.mdev {
561        curves.mdev = Some(compute_curve(
562            &phase,
563            input.tau0_s,
564            &input.options.tau_grid,
565            AllanEstimator::Mdev,
566        )?);
567    }
568    if input.options.estimators.hdev {
569        curves.hdev = Some(compute_curve(
570            &phase,
571            input.tau0_s,
572            &input.options.tau_grid,
573            AllanEstimator::Hdev,
574        )?);
575    }
576    if input.options.estimators.tdev {
577        curves.tdev = Some(compute_curve(
578            &phase,
579            input.tau0_s,
580            &input.options.tau_grid,
581            AllanEstimator::Tdev,
582        )?);
583    }
584
585    Ok(curves)
586}
587
588/// Plain non-overlapping Allan deviation for explicit averaging factors.
589pub fn allan_deviation(
590    series: AllanSeries<'_>,
591    tau0_s: f64,
592    averaging_factors: &[usize],
593) -> Result<AllanResult, AllanError> {
594    compute_explicit(series, tau0_s, averaging_factors, AllanEstimator::Adev)
595}
596
597/// Fully overlapping Allan deviation for explicit averaging factors.
598pub fn overlapping_adev(
599    series: AllanSeries<'_>,
600    tau0_s: f64,
601    averaging_factors: &[usize],
602) -> Result<AllanResult, AllanError> {
603    compute_explicit(
604        series,
605        tau0_s,
606        averaging_factors,
607        AllanEstimator::OverlappingAdev,
608    )
609}
610
611/// Modified Allan deviation for explicit averaging factors.
612pub fn modified_adev(
613    series: AllanSeries<'_>,
614    tau0_s: f64,
615    averaging_factors: &[usize],
616) -> Result<AllanResult, AllanError> {
617    compute_explicit(series, tau0_s, averaging_factors, AllanEstimator::Mdev)
618}
619
620/// Overlapping Hadamard deviation for explicit averaging factors.
621pub fn hadamard_deviation(
622    series: AllanSeries<'_>,
623    tau0_s: f64,
624    averaging_factors: &[usize],
625) -> Result<AllanResult, AllanError> {
626    compute_explicit(series, tau0_s, averaging_factors, AllanEstimator::Hdev)
627}
628
629/// Time deviation for explicit averaging factors.
630pub fn time_deviation(
631    series: AllanSeries<'_>,
632    tau0_s: f64,
633    averaging_factors: &[usize],
634) -> Result<AllanResult, AllanError> {
635    compute_explicit(series, tau0_s, averaging_factors, AllanEstimator::Tdev)
636}
637
638/// Identify per-octave power-law noise and fit PSD coefficients.
639///
640/// The `adev` argument may be plain or overlapping ADEV. The function consumes
641/// the supplied deviation curves as-is and does not recompute any Allan-family
642/// statistic. ADEV identifies random-walk FM, flicker FM, and white FM regions.
643/// For phase modulation, where ADEV has the same `tau^-1` slope for flicker PM
644/// and white PM, the matching MDEV octave supplies the separating slope.
645///
646/// Coefficients are returned as `[h_-2, h_-1, h_0, h_1, h_2]`. FM coefficients
647/// are fitted from ADEV using the IEEE 1139 Allan-variance conversion constants.
648/// PM coefficients are fitted from MDEV using the modified-Allan conversion
649/// constants derived from the NIST SP 1065 MVAR transfer function, with
650/// `measurement_bandwidth_hz` and `basic_tau_s` applied to white PM.
651pub fn fit_power_law_noise(
652    adev: &AllanResult,
653    mdev: &AllanResult,
654    options: PowerLawNoiseOptions,
655) -> Result<PowerLawNoiseFit, PowerLawNoiseError> {
656    validate_power_law_options(options)?;
657    validate_power_law_curve("ADEV", adev)?;
658    validate_power_law_curve("MDEV", mdev)?;
659
660    let dominant_per_octave = classify_power_law_octaves(adev, mdev, options);
661    let regions = build_power_law_regions(&dominant_per_octave, adev, mdev, options);
662    let mut coefficients = [f64::NAN; 5];
663
664    for noise_type in POWER_LAW_NOISE_TYPES {
665        let coefficient = fit_coefficient_for_type(noise_type, &regions, adev, mdev, options);
666        if let Some(coefficient) = coefficient {
667            coefficients[noise_type.coefficient_index()] = coefficient;
668        }
669    }
670
671    Ok(PowerLawNoiseFit {
672        dominant_per_octave,
673        coefficients,
674        regions,
675    })
676}
677
678fn compute_explicit(
679    series: AllanSeries<'_>,
680    tau0_s: f64,
681    averaging_factors: &[usize],
682    estimator: AllanEstimator,
683) -> Result<AllanResult, AllanError> {
684    let phase = prepare_phase(series, tau0_s, GapPolicy::Reject)?;
685    compute_curve(
686        &phase,
687        tau0_s,
688        &TauGrid::Explicit(averaging_factors.to_vec()),
689        estimator,
690    )
691}
692
693const POWER_LAW_NOISE_TYPES: [PowerLawNoiseType; 5] = [
694    PowerLawNoiseType::RandomWalkFM,
695    PowerLawNoiseType::FlickerFM,
696    PowerLawNoiseType::WhiteFM,
697    PowerLawNoiseType::FlickerPM,
698    PowerLawNoiseType::WhitePM,
699];
700
701#[derive(Debug, Clone, Copy, PartialEq, Eq)]
702enum AdevSlopeClass {
703    Noise(PowerLawNoiseType),
704    PhaseModulation,
705    Ambiguous,
706}
707
708fn validate_power_law_options(options: PowerLawNoiseOptions) -> Result<(), PowerLawNoiseError> {
709    if options.min_points_per_octave < 2 {
710        return Err(PowerLawNoiseError::InvalidOptions {
711            field: "min_points_per_octave",
712            reason: "must be at least 2",
713        });
714    }
715    if !(options.slope_tolerance.is_finite() && options.slope_tolerance > 0.0) {
716        return Err(PowerLawNoiseError::InvalidOptions {
717            field: "slope_tolerance",
718            reason: "must be finite and positive",
719        });
720    }
721    if !(options.scatter_tolerance.is_finite() && options.scatter_tolerance >= 0.0) {
722        return Err(PowerLawNoiseError::InvalidOptions {
723            field: "scatter_tolerance",
724            reason: "must be finite and nonnegative",
725        });
726    }
727    if !(options.basic_tau_s.is_finite() && options.basic_tau_s > 0.0) {
728        return Err(PowerLawNoiseError::InvalidOptions {
729            field: "basic_tau_s",
730            reason: "must be finite and positive",
731        });
732    }
733    if !(options.measurement_bandwidth_hz.is_finite() && options.measurement_bandwidth_hz > 0.0) {
734        return Err(PowerLawNoiseError::InvalidOptions {
735            field: "measurement_bandwidth_hz",
736            reason: "must be finite and positive",
737        });
738    }
739    Ok(())
740}
741
742fn validate_power_law_curve(
743    curve_name: &'static str,
744    curve: &AllanResult,
745) -> Result<(), PowerLawNoiseError> {
746    if curve.tau_s.len() != curve.deviation.len() || curve.tau_s.len() != curve.n.len() {
747        return Err(PowerLawNoiseError::InvalidCurve {
748            curve: curve_name,
749            reason: "tau, deviation, and term-count lengths must match",
750        });
751    }
752
753    let mut previous_tau_s = None;
754    for (index, (&tau_s, &deviation)) in curve.tau_s.iter().zip(curve.deviation.iter()).enumerate()
755    {
756        if !(tau_s.is_finite() && tau_s > 0.0) {
757            return Err(PowerLawNoiseError::InvalidCurve {
758                curve: curve_name,
759                reason: "tau values must be finite and positive",
760            });
761        }
762        if previous_tau_s.is_some_and(|previous| tau_s <= previous) {
763            return Err(PowerLawNoiseError::InvalidCurve {
764                curve: curve_name,
765                reason: "tau values must be strictly increasing",
766            });
767        }
768        previous_tau_s = Some(tau_s);
769
770        if !(deviation.is_finite() && deviation >= 0.0) {
771            return Err(PowerLawNoiseError::InvalidCurve {
772                curve: curve_name,
773                reason: "deviations must be finite and nonnegative",
774            });
775        }
776        if curve.n[index] == 0 {
777            return Err(PowerLawNoiseError::InvalidCurve {
778                curve: curve_name,
779                reason: "term counts must be positive",
780            });
781        }
782    }
783
784    Ok(())
785}
786
787fn classify_power_law_octaves(
788    adev: &AllanResult,
789    mdev: &AllanResult,
790    options: PowerLawNoiseOptions,
791) -> Vec<PowerLawOctave> {
792    let mut octaves = Vec::new();
793    let mut start = 0usize;
794    while start < adev.tau_s.len() {
795        let end = octave_end_index(&adev.tau_s, start);
796        let point_count = end + 1 - start;
797        if point_count < options.min_points_per_octave {
798            let tau_start_s = adev.tau_s[start];
799            octaves.push(PowerLawOctave {
800                tau_start_s,
801                tau_end_s: tau_start_s,
802                point_count,
803                adev_slope: None,
804                mdev_slope: None,
805                slope_scatter: None,
806                dominance: PowerLawOctaveDominance::Flagged(PowerLawOctaveFlag::UnderSampled),
807            });
808            start += 1;
809            continue;
810        }
811
812        octaves.push(classify_power_law_window(start, end, adev, mdev, options));
813        if end >= adev.tau_s.len() - 1 {
814            break;
815        }
816        start = end;
817    }
818    octaves
819}
820
821fn octave_end_index(tau_s: &[f64], start: usize) -> usize {
822    let tau_limit_s = tau_s[start] * 2.0;
823    let tolerance = tau_limit_s.abs().max(1.0) * 32.0 * f64::EPSILON;
824    let mut end = start;
825    while end + 1 < tau_s.len() && tau_s[end + 1] <= tau_limit_s + tolerance {
826        end += 1;
827    }
828    end
829}
830
831fn classify_power_law_window(
832    start: usize,
833    end: usize,
834    adev: &AllanResult,
835    mdev: &AllanResult,
836    options: PowerLawNoiseOptions,
837) -> PowerLawOctave {
838    let tau_start_s = adev.tau_s[start];
839    let tau_end_s = adev.tau_s[end];
840    let point_count = end + 1 - start;
841
842    let Some(adev_slope) = log_log_slope_for_curve(adev, start, end) else {
843        return PowerLawOctave {
844            tau_start_s,
845            tau_end_s,
846            point_count,
847            adev_slope: None,
848            mdev_slope: None,
849            slope_scatter: None,
850            dominance: PowerLawOctaveDominance::Flagged(PowerLawOctaveFlag::DegenerateDeviation),
851        };
852    };
853
854    let adjacent_slopes = adjacent_log_log_slopes(adev, start, end);
855    let slope_scatter = robust_slope_scatter(&adjacent_slopes);
856    let scatter_is_ambiguous =
857        slope_scatter.is_some_and(|scatter| scatter > options.scatter_tolerance);
858    let adev_class = classify_adev_slope(adev_slope, options.slope_tolerance);
859    let local_adev_consistent =
860        local_adev_slopes_consistent(&adjacent_slopes, adev_class, options.slope_tolerance);
861
862    let (mdev_slope, mdev_scatter, local_mdev_consistent) =
863        mdev_slope_in_range(mdev, tau_start_s, tau_end_s, options);
864
865    let dominance = if scatter_is_ambiguous || !local_adev_consistent {
866        PowerLawOctaveDominance::Ambiguous
867    } else {
868        match adev_class {
869            AdevSlopeClass::Noise(noise_type) => {
870                if let Some(mdev_slope) = mdev_slope {
871                    match classify_mdev_slope(mdev_slope, options.slope_tolerance) {
872                        Some(mdev_type) if mdev_type != noise_type => {
873                            PowerLawOctaveDominance::Ambiguous
874                        }
875                        None => PowerLawOctaveDominance::Ambiguous,
876                        Some(_) => PowerLawOctaveDominance::Dominant(noise_type),
877                    }
878                } else {
879                    PowerLawOctaveDominance::Dominant(noise_type)
880                }
881            }
882            AdevSlopeClass::PhaseModulation => match mdev_slope {
883                Some(mdev_slope)
884                    if !mdev_scatter.is_some_and(|scatter| scatter > options.scatter_tolerance)
885                        && local_mdev_consistent =>
886                {
887                    match classify_mdev_slope(mdev_slope, options.slope_tolerance) {
888                        Some(PowerLawNoiseType::FlickerPM) => {
889                            PowerLawOctaveDominance::Dominant(PowerLawNoiseType::FlickerPM)
890                        }
891                        Some(PowerLawNoiseType::WhitePM) => {
892                            PowerLawOctaveDominance::Dominant(PowerLawNoiseType::WhitePM)
893                        }
894                        _ => PowerLawOctaveDominance::Ambiguous,
895                    }
896                }
897                Some(_) => PowerLawOctaveDominance::Ambiguous,
898                None => PowerLawOctaveDominance::Flagged(PowerLawOctaveFlag::MissingModifiedAllan),
899            },
900            AdevSlopeClass::Ambiguous => PowerLawOctaveDominance::Ambiguous,
901        }
902    };
903
904    PowerLawOctave {
905        tau_start_s,
906        tau_end_s,
907        point_count,
908        adev_slope: Some(adev_slope),
909        mdev_slope,
910        slope_scatter,
911        dominance,
912    }
913}
914
915fn log_log_slope_for_curve(curve: &AllanResult, start: usize, end: usize) -> Option<f64> {
916    if end <= start {
917        return None;
918    }
919    let tau = &curve.tau_s[start..=end];
920    let deviation = &curve.deviation[start..=end];
921    if deviation.iter().any(|&value| value <= 0.0) {
922        return None;
923    }
924
925    let n = tau.len() as f64;
926    let (sum_x, sum_y) = tau
927        .iter()
928        .zip(deviation.iter())
929        .fold((0.0, 0.0), |(sum_x, sum_y), (&tau_s, &sigma)| {
930            (sum_x + tau_s.ln(), sum_y + sigma.ln())
931        });
932    let mean_x = sum_x / n;
933    let mean_y = sum_y / n;
934
935    let (num, den) =
936        tau.iter()
937            .zip(deviation.iter())
938            .fold((0.0, 0.0), |(num, den), (&tau_s, &sigma)| {
939                let dx = tau_s.ln() - mean_x;
940                let dy = sigma.ln() - mean_y;
941                (num + dx * dy, den + dx * dx)
942            });
943
944    if den > 0.0 {
945        Some(num / den)
946    } else {
947        None
948    }
949}
950
951fn adjacent_log_log_slopes(curve: &AllanResult, start: usize, end: usize) -> Vec<f64> {
952    let mut slopes = Vec::new();
953    for index in start..end {
954        let tau0 = curve.tau_s[index];
955        let tau1 = curve.tau_s[index + 1];
956        let sigma0 = curve.deviation[index];
957        let sigma1 = curve.deviation[index + 1];
958        if sigma0 <= 0.0 || sigma1 <= 0.0 {
959            continue;
960        }
961        let denominator = tau1.ln() - tau0.ln();
962        if denominator > 0.0 {
963            slopes.push((sigma1.ln() - sigma0.ln()) / denominator);
964        }
965    }
966    slopes
967}
968
969fn robust_slope_scatter(slopes: &[f64]) -> Option<f64> {
970    if slopes.len() < 2 {
971        return Some(0.0);
972    }
973    mad_spread(slopes, 0.0).ok()
974}
975
976fn local_adev_slopes_consistent(slopes: &[f64], expected: AdevSlopeClass, tolerance: f64) -> bool {
977    slopes
978        .iter()
979        .all(|&slope| classify_adev_slope(slope, tolerance) == expected)
980}
981
982fn local_mdev_slopes_consistent(
983    slopes: &[f64],
984    expected: Option<PowerLawNoiseType>,
985    tolerance: f64,
986) -> bool {
987    match expected {
988        Some(expected) => slopes
989            .iter()
990            .all(|&slope| classify_mdev_slope(slope, tolerance) == Some(expected)),
991        None => true,
992    }
993}
994
995fn mdev_slope_in_range(
996    mdev: &AllanResult,
997    tau_start_s: f64,
998    tau_end_s: f64,
999    options: PowerLawNoiseOptions,
1000) -> (Option<f64>, Option<f64>, bool) {
1001    let indices = curve_indices_in_range(mdev, tau_start_s, tau_end_s);
1002    if indices.len() < options.min_points_per_octave {
1003        return (None, None, false);
1004    }
1005    let start = indices[0];
1006    let end = *indices.last().expect("nonempty indices");
1007    let Some(slope) = log_log_slope_for_curve(mdev, start, end) else {
1008        return (None, None, false);
1009    };
1010    let adjacent = adjacent_log_log_slopes(mdev, start, end);
1011    let classified = classify_mdev_slope(slope, options.slope_tolerance);
1012    let consistent = local_mdev_slopes_consistent(&adjacent, classified, options.slope_tolerance);
1013    (Some(slope), robust_slope_scatter(&adjacent), consistent)
1014}
1015
1016fn curve_indices_in_range(curve: &AllanResult, tau_start_s: f64, tau_end_s: f64) -> Vec<usize> {
1017    let tolerance = tau_end_s.abs().max(tau_start_s.abs()).max(1.0) * 32.0 * f64::EPSILON;
1018    curve
1019        .tau_s
1020        .iter()
1021        .enumerate()
1022        .filter_map(|(index, &tau_s)| {
1023            if tau_s + tolerance >= tau_start_s && tau_s <= tau_end_s + tolerance {
1024                Some(index)
1025            } else {
1026                None
1027            }
1028        })
1029        .collect()
1030}
1031
1032fn classify_adev_slope(slope: f64, tolerance: f64) -> AdevSlopeClass {
1033    let phase_target = -1.0;
1034    if (slope - phase_target).abs() <= tolerance {
1035        return AdevSlopeClass::PhaseModulation;
1036    }
1037
1038    let candidates = [
1039        PowerLawNoiseType::RandomWalkFM,
1040        PowerLawNoiseType::FlickerFM,
1041        PowerLawNoiseType::WhiteFM,
1042    ];
1043    let mut best = None;
1044    let mut best_distance = f64::INFINITY;
1045    for noise_type in candidates {
1046        let distance = (slope - allan_deviation_power_law_slope(noise_type)).abs();
1047        if distance < best_distance {
1048            best = Some(noise_type);
1049            best_distance = distance;
1050        }
1051    }
1052    if best_distance <= tolerance {
1053        AdevSlopeClass::Noise(best.expect("candidate selected"))
1054    } else {
1055        AdevSlopeClass::Ambiguous
1056    }
1057}
1058
1059fn classify_mdev_slope(slope: f64, tolerance: f64) -> Option<PowerLawNoiseType> {
1060    let mut best = None;
1061    let mut best_distance = f64::INFINITY;
1062    for noise_type in POWER_LAW_NOISE_TYPES {
1063        let distance = (slope - modified_allan_deviation_power_law_slope(noise_type)).abs();
1064        if distance < best_distance {
1065            best = Some(noise_type);
1066            best_distance = distance;
1067        }
1068    }
1069    if best_distance <= tolerance {
1070        best
1071    } else {
1072        None
1073    }
1074}
1075
1076fn build_power_law_regions(
1077    octaves: &[PowerLawOctave],
1078    adev: &AllanResult,
1079    mdev: &AllanResult,
1080    options: PowerLawNoiseOptions,
1081) -> Vec<PowerLawNoiseRegion> {
1082    let mut regions = Vec::new();
1083    let mut current: Option<PowerLawNoiseRegion> = None;
1084
1085    for octave in octaves {
1086        let PowerLawOctaveDominance::Dominant(noise_type) = octave.dominance else {
1087            if let Some(region) = current.take() {
1088                regions.push(region);
1089            }
1090            continue;
1091        };
1092        let Some(slope) = octave_slope_for_region(noise_type, octave) else {
1093            continue;
1094        };
1095
1096        if current
1097            .as_ref()
1098            .is_some_and(|region| region.noise_type == noise_type)
1099        {
1100            let region = current.as_mut().expect("current region");
1101            let next_count = region.octave_count + 1;
1102            region.tau_end_s = octave.tau_end_s;
1103            region.mean_slope =
1104                (region.mean_slope * region.octave_count as f64 + slope) / next_count as f64;
1105            region.octave_count = next_count;
1106        } else {
1107            if let Some(region) = current.take() {
1108                regions.push(region);
1109            }
1110            current = Some(PowerLawNoiseRegion {
1111                noise_type,
1112                tau_start_s: octave.tau_start_s,
1113                tau_end_s: octave.tau_end_s,
1114                octave_count: 1,
1115                point_count: 0,
1116                mean_slope: slope,
1117                coefficient: f64::NAN,
1118            });
1119        }
1120    }
1121
1122    if let Some(region) = current {
1123        regions.push(region);
1124    }
1125
1126    for region in &mut regions {
1127        region.point_count = count_points_for_region(region.noise_type, region, adev, mdev);
1128        if let Some(coefficient) = fit_coefficient_for_ranges(
1129            region.noise_type,
1130            &[(region.tau_start_s, region.tau_end_s)],
1131            adev,
1132            mdev,
1133            options,
1134        ) {
1135            region.coefficient = coefficient;
1136        }
1137    }
1138
1139    regions
1140}
1141
1142fn octave_slope_for_region(noise_type: PowerLawNoiseType, octave: &PowerLawOctave) -> Option<f64> {
1143    match noise_type {
1144        PowerLawNoiseType::FlickerPM | PowerLawNoiseType::WhitePM => octave.mdev_slope,
1145        _ => octave.adev_slope,
1146    }
1147}
1148
1149fn count_points_for_region(
1150    noise_type: PowerLawNoiseType,
1151    region: &PowerLawNoiseRegion,
1152    adev: &AllanResult,
1153    mdev: &AllanResult,
1154) -> usize {
1155    let curve = coefficient_curve(noise_type, adev, mdev);
1156    curve_indices_in_range(curve, region.tau_start_s, region.tau_end_s).len()
1157}
1158
1159fn fit_coefficient_for_type(
1160    noise_type: PowerLawNoiseType,
1161    regions: &[PowerLawNoiseRegion],
1162    adev: &AllanResult,
1163    mdev: &AllanResult,
1164    options: PowerLawNoiseOptions,
1165) -> Option<f64> {
1166    let ranges = regions
1167        .iter()
1168        .filter(|region| region.noise_type == noise_type)
1169        .map(|region| (region.tau_start_s, region.tau_end_s))
1170        .collect::<Vec<_>>();
1171    fit_coefficient_for_ranges(noise_type, &ranges, adev, mdev, options)
1172}
1173
1174fn fit_coefficient_for_ranges(
1175    noise_type: PowerLawNoiseType,
1176    ranges: &[(f64, f64)],
1177    adev: &AllanResult,
1178    mdev: &AllanResult,
1179    options: PowerLawNoiseOptions,
1180) -> Option<f64> {
1181    if ranges.is_empty() {
1182        return None;
1183    }
1184
1185    let curve = coefficient_curve(noise_type, adev, mdev);
1186    let mut sum_ab = 0.0;
1187    let mut sum_aa = 0.0;
1188    for (index, &tau_s) in curve.tau_s.iter().enumerate() {
1189        if !ranges
1190            .iter()
1191            .any(|&(start, end)| tau_in_range(tau_s, start, end))
1192        {
1193            continue;
1194        }
1195        let factor = power_law_variance_factor(noise_type, tau_s, options)?;
1196        if !(factor.is_finite() && factor > 0.0) {
1197            return None;
1198        }
1199        let variance = curve.deviation[index] * curve.deviation[index];
1200        if !(variance.is_finite() && variance >= 0.0) {
1201            return None;
1202        }
1203        sum_ab += factor * variance;
1204        sum_aa += factor * factor;
1205    }
1206
1207    if sum_aa > 0.0 {
1208        Some(sum_ab / sum_aa)
1209    } else {
1210        None
1211    }
1212}
1213
1214fn coefficient_curve<'a>(
1215    noise_type: PowerLawNoiseType,
1216    adev: &'a AllanResult,
1217    mdev: &'a AllanResult,
1218) -> &'a AllanResult {
1219    match noise_type {
1220        PowerLawNoiseType::FlickerPM | PowerLawNoiseType::WhitePM => mdev,
1221        _ => adev,
1222    }
1223}
1224
1225fn tau_in_range(tau_s: f64, tau_start_s: f64, tau_end_s: f64) -> bool {
1226    let tolerance = tau_end_s.abs().max(tau_start_s.abs()).max(1.0) * 32.0 * f64::EPSILON;
1227    tau_s + tolerance >= tau_start_s && tau_s <= tau_end_s + tolerance
1228}
1229
1230fn power_law_variance_factor(
1231    noise_type: PowerLawNoiseType,
1232    tau_s: f64,
1233    options: PowerLawNoiseOptions,
1234) -> Option<f64> {
1235    if !(tau_s.is_finite() && tau_s > 0.0) {
1236        return None;
1237    }
1238    let pi = core::f64::consts::PI;
1239    let factor = match noise_type {
1240        PowerLawNoiseType::RandomWalkFM => (2.0 * pi * pi / 3.0) * tau_s,
1241        PowerLawNoiseType::FlickerFM => 2.0 * core::f64::consts::LN_2,
1242        PowerLawNoiseType::WhiteFM => 0.5 / tau_s,
1243        PowerLawNoiseType::FlickerPM => {
1244            let numerator = 3.0 * (256.0_f64 / 27.0).ln();
1245            numerator / (8.0 * pi * pi * tau_s * tau_s)
1246        }
1247        PowerLawNoiseType::WhitePM => {
1248            let numerator = 3.0 * options.measurement_bandwidth_hz * options.basic_tau_s;
1249            numerator / (4.0 * pi * pi * tau_s * tau_s * tau_s)
1250        }
1251    };
1252    if factor.is_finite() && factor > 0.0 {
1253        Some(factor)
1254    } else {
1255        None
1256    }
1257}
1258
1259#[derive(Debug, Clone, Copy)]
1260struct PhasePoint {
1261    value_s: f64,
1262    run_id: usize,
1263}
1264
1265fn prepare_phase(
1266    series: AllanSeries<'_>,
1267    tau0_s: f64,
1268    gap_policy: GapPolicy,
1269) -> Result<Vec<Option<PhasePoint>>, AllanError> {
1270    validate_tau0(tau0_s)?;
1271    match series {
1272        AllanSeries::PhaseSeconds(values) => phase_from_contiguous(values),
1273        AllanSeries::FractionalFrequency(values) => phase_from_contiguous_frequency(values, tau0_s),
1274        AllanSeries::PhaseSecondsWithGaps(values) => phase_from_gapped(values, gap_policy),
1275        AllanSeries::FractionalFrequencyWithGaps(values) => {
1276            phase_from_gapped_frequency(values, tau0_s, gap_policy)
1277        }
1278    }
1279}
1280
1281fn validate_tau0(tau0_s: f64) -> Result<(), AllanError> {
1282    if tau0_s.is_finite() && tau0_s > 0.0 {
1283        Ok(())
1284    } else {
1285        Err(AllanError::InvalidTau0 { tau0_s })
1286    }
1287}
1288
1289fn phase_from_contiguous(values: &[f64]) -> Result<Vec<Option<PhasePoint>>, AllanError> {
1290    if values.is_empty() {
1291        return Err(AllanError::EmptySeries);
1292    }
1293    values
1294        .iter()
1295        .enumerate()
1296        .map(|(index, &value_s)| {
1297            validate_sample(index, value_s).map(|value_s| Some(PhasePoint { value_s, run_id: 0 }))
1298        })
1299        .collect()
1300}
1301
1302fn phase_from_contiguous_frequency(
1303    values: &[f64],
1304    tau0_s: f64,
1305) -> Result<Vec<Option<PhasePoint>>, AllanError> {
1306    if values.is_empty() {
1307        return Err(AllanError::EmptySeries);
1308    }
1309    let mut phase = Vec::with_capacity(values.len() + 1);
1310    let mut value_s = 0.0;
1311    phase.push(Some(PhasePoint { value_s, run_id: 0 }));
1312    for (index, &frequency) in values.iter().enumerate() {
1313        let frequency = validate_sample(index, frequency)?;
1314        value_s += tau0_s * frequency;
1315        if !value_s.is_finite() {
1316            return Err(AllanError::NonFiniteSample { index });
1317        }
1318        phase.push(Some(PhasePoint { value_s, run_id: 0 }));
1319    }
1320    Ok(phase)
1321}
1322
1323fn phase_from_gapped(
1324    values: &[Option<f64>],
1325    gap_policy: GapPolicy,
1326) -> Result<Vec<Option<PhasePoint>>, AllanError> {
1327    if values.is_empty() {
1328        return Err(AllanError::EmptySeries);
1329    }
1330
1331    let mut phase = Vec::with_capacity(values.len());
1332    let mut run_id = 0usize;
1333    let mut in_run = false;
1334    for (index, value) in values.iter().enumerate() {
1335        match value {
1336            Some(value_s) => {
1337                let value_s = validate_sample(index, *value_s)?;
1338                if !in_run {
1339                    run_id += 1;
1340                    in_run = true;
1341                }
1342                phase.push(Some(PhasePoint { value_s, run_id }));
1343            }
1344            None => {
1345                if gap_policy == GapPolicy::Reject {
1346                    return Err(AllanError::Gap { index });
1347                }
1348                in_run = false;
1349                phase.push(None);
1350            }
1351        }
1352    }
1353    Ok(phase)
1354}
1355
1356fn phase_from_gapped_frequency(
1357    values: &[Option<f64>],
1358    tau0_s: f64,
1359    gap_policy: GapPolicy,
1360) -> Result<Vec<Option<PhasePoint>>, AllanError> {
1361    if values.is_empty() {
1362        return Err(AllanError::EmptySeries);
1363    }
1364    if gap_policy == GapPolicy::Reject {
1365        for (index, value) in values.iter().enumerate() {
1366            if value.is_none() {
1367                return Err(AllanError::Gap { index });
1368            }
1369        }
1370    }
1371
1372    let mut phase = vec![None; values.len() + 1];
1373    let mut run_id = 0usize;
1374    let mut index = 0usize;
1375    while index < values.len() {
1376        if values[index].is_none() {
1377            index += 1;
1378            continue;
1379        };
1380
1381        run_id += 1;
1382        let mut value_s = 0.0;
1383        phase[index] = Some(PhasePoint { value_s, run_id });
1384        let mut sample_index = index;
1385        while let Some(current) = values.get(sample_index).copied().flatten() {
1386            let current = validate_sample(sample_index, current)?;
1387            value_s += tau0_s * current;
1388            if !value_s.is_finite() {
1389                return Err(AllanError::NonFiniteSample {
1390                    index: sample_index,
1391                });
1392            }
1393            phase[sample_index + 1] = Some(PhasePoint { value_s, run_id });
1394            sample_index += 1;
1395        }
1396        index = sample_index + 1;
1397    }
1398
1399    Ok(phase)
1400}
1401
1402fn validate_sample(index: usize, value: f64) -> Result<f64, AllanError> {
1403    if value.is_finite() {
1404        Ok(value)
1405    } else {
1406        Err(AllanError::NonFiniteSample { index })
1407    }
1408}
1409
1410fn compute_curve(
1411    phase: &[Option<PhasePoint>],
1412    tau0_s: f64,
1413    tau_grid: &TauGrid,
1414    estimator: AllanEstimator,
1415) -> Result<AllanResult, AllanError> {
1416    let averaging_factors = averaging_factors(phase.len(), estimator, tau_grid)?;
1417    let strict = matches!(tau_grid, TauGrid::Explicit(_));
1418    let mut result = AllanResult::new();
1419
1420    for m in averaging_factors {
1421        if m == 0 {
1422            return Err(AllanError::InvalidAveragingFactor {
1423                averaging_factor: m,
1424            });
1425        }
1426        if candidate_count(phase.len(), estimator, m).is_none() {
1427            if strict {
1428                return Err(AllanError::TooFewSamples {
1429                    estimator,
1430                    averaging_factor: m,
1431                    available_phase_samples: phase.len(),
1432                });
1433            }
1434            continue;
1435        }
1436
1437        let tau_s = tau0_s * m as f64;
1438        if !tau_s.is_finite() {
1439            return Err(AllanError::NonFiniteTau {
1440                estimator,
1441                averaging_factor: m,
1442            });
1443        }
1444
1445        let (sum_sq, n) = estimator_sum_squares(phase, estimator, m);
1446        if n == 0 {
1447            if strict {
1448                return Err(AllanError::TooFewSamples {
1449                    estimator,
1450                    averaging_factor: m,
1451                    available_phase_samples: phase.len(),
1452                });
1453            }
1454            continue;
1455        }
1456
1457        let deviation = deviation_from_sum(sum_sq, n, tau_s, m, estimator);
1458        if !deviation.is_finite() {
1459            return Err(AllanError::NonFiniteDeviation {
1460                estimator,
1461                averaging_factor: m,
1462            });
1463        }
1464        result.push(tau_s, deviation, n);
1465    }
1466
1467    if result.is_empty() {
1468        return Err(AllanError::TooFewSamples {
1469            estimator,
1470            averaging_factor: 1,
1471            available_phase_samples: phase.len(),
1472        });
1473    }
1474    Ok(result)
1475}
1476
1477fn averaging_factors(
1478    phase_len: usize,
1479    estimator: AllanEstimator,
1480    tau_grid: &TauGrid,
1481) -> Result<Vec<usize>, AllanError> {
1482    match tau_grid {
1483        TauGrid::Explicit(values) => {
1484            if values.is_empty() {
1485                Err(AllanError::EmptyTauGrid)
1486            } else {
1487                Ok(values.clone())
1488            }
1489        }
1490        TauGrid::All => Ok((1..=max_averaging_factor(phase_len, estimator)).collect()),
1491        TauGrid::Octave => {
1492            let max_m = max_averaging_factor(phase_len, estimator);
1493            let mut values = Vec::new();
1494            let mut m = 1usize;
1495            while m <= max_m {
1496                values.push(m);
1497                if m > max_m / 2 {
1498                    break;
1499                }
1500                m *= 2;
1501            }
1502            Ok(values)
1503        }
1504    }
1505}
1506
1507fn max_averaging_factor(phase_len: usize, estimator: AllanEstimator) -> usize {
1508    match estimator {
1509        AllanEstimator::Adev | AllanEstimator::OverlappingAdev => phase_len.saturating_sub(1) / 2,
1510        AllanEstimator::Mdev | AllanEstimator::Tdev => phase_len / 3,
1511        AllanEstimator::Hdev => phase_len.saturating_sub(1) / 3,
1512    }
1513}
1514
1515fn candidate_count(phase_len: usize, estimator: AllanEstimator, m: usize) -> Option<usize> {
1516    if m == 0 {
1517        return None;
1518    }
1519    match estimator {
1520        AllanEstimator::Adev => {
1521            let frequency_len = phase_len.checked_sub(1)?;
1522            (frequency_len / m).checked_sub(1)
1523        }
1524        AllanEstimator::OverlappingAdev => phase_len.checked_sub(m.checked_mul(2)?),
1525        AllanEstimator::Mdev | AllanEstimator::Tdev => {
1526            phase_len.checked_sub(m.checked_mul(3)?)?.checked_add(1)
1527        }
1528        AllanEstimator::Hdev => phase_len.checked_sub(m.checked_mul(3)?),
1529    }
1530}
1531
1532fn estimator_sum_squares(
1533    phase: &[Option<PhasePoint>],
1534    estimator: AllanEstimator,
1535    m: usize,
1536) -> (f64, usize) {
1537    match estimator {
1538        AllanEstimator::Adev => plain_adev_sum_squares(phase, m),
1539        AllanEstimator::OverlappingAdev => overlapping_adev_sum_squares(phase, m),
1540        AllanEstimator::Mdev | AllanEstimator::Tdev => modified_adev_sum_squares(phase, m),
1541        AllanEstimator::Hdev => hadamard_sum_squares(phase, m),
1542    }
1543}
1544
1545fn deviation_from_sum(
1546    sum_sq: f64,
1547    n: usize,
1548    tau_s: f64,
1549    m: usize,
1550    estimator: AllanEstimator,
1551) -> f64 {
1552    let n = n as f64;
1553    match estimator {
1554        AllanEstimator::Adev | AllanEstimator::OverlappingAdev => {
1555            (sum_sq / (2.0 * n * tau_s * tau_s)).sqrt()
1556        }
1557        AllanEstimator::Mdev => {
1558            let mf = m as f64;
1559            (sum_sq / (2.0 * mf * mf * n * tau_s * tau_s)).sqrt()
1560        }
1561        AllanEstimator::Hdev => (sum_sq / (6.0 * n * tau_s * tau_s)).sqrt(),
1562        AllanEstimator::Tdev => {
1563            let mf = m as f64;
1564            let mdev = (sum_sq / (2.0 * mf * mf * n * tau_s * tau_s)).sqrt();
1565            tau_s * mdev / SQRT_3
1566        }
1567    }
1568}
1569
1570fn plain_adev_sum_squares(phase: &[Option<PhasePoint>], m: usize) -> (f64, usize) {
1571    let Some(count) = candidate_count(phase.len(), AllanEstimator::Adev, m) else {
1572        return (0.0, 0);
1573    };
1574    let mut sum_sq = 0.0;
1575    let mut n = 0usize;
1576    for j in 0..count {
1577        let i = j * m;
1578        if let Some((diff, _)) = second_difference(phase, i, m) {
1579            let square = diff * diff;
1580            sum_sq += square;
1581            n += 1;
1582        }
1583    }
1584    (sum_sq, n)
1585}
1586
1587fn overlapping_adev_sum_squares(phase: &[Option<PhasePoint>], m: usize) -> (f64, usize) {
1588    let Some(count) = candidate_count(phase.len(), AllanEstimator::OverlappingAdev, m) else {
1589        return (0.0, 0);
1590    };
1591    let mut sum_sq = 0.0;
1592    let mut n = 0usize;
1593    for i in 0..count {
1594        if let Some((diff, _)) = second_difference(phase, i, m) {
1595            let square = diff * diff;
1596            sum_sq += square;
1597            n += 1;
1598        }
1599    }
1600    (sum_sq, n)
1601}
1602
1603fn modified_adev_sum_squares(phase: &[Option<PhasePoint>], m: usize) -> (f64, usize) {
1604    let Some(count) = candidate_count(phase.len(), AllanEstimator::Mdev, m) else {
1605        return (0.0, 0);
1606    };
1607    let mut sum_sq = 0.0;
1608    let mut n = 0usize;
1609    for j in 0..count {
1610        let mut inner = 0.0;
1611        let mut run_id = None;
1612        let mut valid = true;
1613        for i in j..(j + m) {
1614            let Some((diff, diff_run_id)) = second_difference(phase, i, m) else {
1615                valid = false;
1616                break;
1617            };
1618            if run_id.is_some_and(|current| current != diff_run_id) {
1619                valid = false;
1620                break;
1621            }
1622            run_id = Some(diff_run_id);
1623            inner += diff;
1624        }
1625        if valid {
1626            let square = inner * inner;
1627            sum_sq += square;
1628            n += 1;
1629        }
1630    }
1631    (sum_sq, n)
1632}
1633
1634fn hadamard_sum_squares(phase: &[Option<PhasePoint>], m: usize) -> (f64, usize) {
1635    let Some(count) = candidate_count(phase.len(), AllanEstimator::Hdev, m) else {
1636        return (0.0, 0);
1637    };
1638    let mut sum_sq = 0.0;
1639    let mut n = 0usize;
1640    for i in 0..count {
1641        if let Some(diff) = third_difference(phase, i, m) {
1642            let square = diff * diff;
1643            sum_sq += square;
1644            n += 1;
1645        }
1646    }
1647    (sum_sq, n)
1648}
1649
1650fn second_difference(phase: &[Option<PhasePoint>], i: usize, m: usize) -> Option<(f64, usize)> {
1651    let x0 = phase.get(i).copied().flatten()?;
1652    let x1 = phase.get(i + m).copied().flatten()?;
1653    let x2 = phase.get(i + 2 * m).copied().flatten()?;
1654    if x0.run_id != x1.run_id || x0.run_id != x2.run_id {
1655        return None;
1656    }
1657    Some((x2.value_s - 2.0 * x1.value_s + x0.value_s, x0.run_id))
1658}
1659
1660fn third_difference(phase: &[Option<PhasePoint>], i: usize, m: usize) -> Option<f64> {
1661    let x0 = phase.get(i).copied().flatten()?;
1662    let x1 = phase.get(i + m).copied().flatten()?;
1663    let x2 = phase.get(i + 2 * m).copied().flatten()?;
1664    let x3 = phase.get(i + 3 * m).copied().flatten()?;
1665    if x0.run_id != x1.run_id || x0.run_id != x2.run_id || x0.run_id != x3.run_id {
1666        return None;
1667    }
1668    Some(x3.value_s - 3.0 * x2.value_s + 3.0 * x1.value_s - x0.value_s)
1669}