1use 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#[derive(Debug, Clone, Copy, PartialEq)]
21pub enum AllanSeries<'a> {
22 PhaseSeconds(&'a [f64]),
24 FractionalFrequency(&'a [f64]),
26 PhaseSecondsWithGaps(&'a [Option<f64>]),
28 FractionalFrequencyWithGaps(&'a [Option<f64>]),
30}
31
32#[derive(Debug, Clone, PartialEq, Eq, Default)]
34pub enum TauGrid {
35 #[default]
37 Octave,
38 All,
40 Explicit(Vec<usize>),
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
46pub enum GapPolicy {
47 #[default]
49 Reject,
50 OmitTerms,
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub struct AllanEstimatorSet {
57 pub adev: bool,
59 pub overlapping_adev: bool,
61 pub mdev: bool,
63 pub hdev: bool,
65 pub tdev: bool,
67}
68
69impl AllanEstimatorSet {
70 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 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 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#[derive(Debug, Clone, PartialEq, Eq, Default)]
116pub struct AllanOptions {
117 pub estimators: AllanEstimatorSet,
119 pub tau_grid: TauGrid,
121 pub gap_policy: GapPolicy,
123}
124
125#[derive(Debug, Clone, PartialEq)]
127pub struct AllanInput<'a> {
128 pub series: AllanSeries<'a>,
130 pub tau0_s: f64,
132 pub options: AllanOptions,
134}
135
136#[derive(Debug, Clone, PartialEq)]
138pub struct AllanResult {
139 pub tau_s: Vec<f64>,
141 pub deviation: Vec<f64>,
143 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 pub fn len(&self) -> usize {
164 self.tau_s.len()
165 }
166
167 pub fn is_empty(&self) -> bool {
169 self.tau_s.is_empty()
170 }
171}
172
173#[derive(Debug, Clone, PartialEq, Default)]
175pub struct AllanDeviationCurves {
176 pub adev: Option<AllanResult>,
178 pub overlapping_adev: Option<AllanResult>,
180 pub mdev: Option<AllanResult>,
182 pub hdev: Option<AllanResult>,
184 pub tdev: Option<AllanResult>,
186}
187
188#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
190pub enum PowerLawNoiseType {
191 RandomWalkFM,
193 FlickerFM,
195 WhiteFM,
197 FlickerPM,
199 WhitePM,
201}
202
203impl PowerLawNoiseType {
204 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 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
227pub 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
237pub 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
248pub 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#[derive(Debug, Clone, Copy, PartialEq)]
260pub struct PowerLawNoiseOptions {
261 pub min_points_per_octave: usize,
263 pub slope_tolerance: f64,
265 pub scatter_tolerance: f64,
267 pub basic_tau_s: f64,
269 pub measurement_bandwidth_hz: f64,
271}
272
273impl PowerLawNoiseOptions {
274 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
293pub enum PowerLawOctaveFlag {
294 UnderSampled,
296 DegenerateDeviation,
298 MissingModifiedAllan,
300}
301
302#[derive(Debug, Clone, Copy, PartialEq, Eq)]
304pub enum PowerLawOctaveDominance {
305 Dominant(PowerLawNoiseType),
307 Ambiguous,
309 Flagged(PowerLawOctaveFlag),
311}
312
313#[derive(Debug, Clone, PartialEq)]
315pub struct PowerLawOctave {
316 pub tau_start_s: f64,
318 pub tau_end_s: f64,
320 pub point_count: usize,
322 pub adev_slope: Option<f64>,
324 pub mdev_slope: Option<f64>,
326 pub slope_scatter: Option<f64>,
328 pub dominance: PowerLawOctaveDominance,
330}
331
332#[derive(Debug, Clone, PartialEq)]
334pub struct PowerLawNoiseRegion {
335 pub noise_type: PowerLawNoiseType,
337 pub tau_start_s: f64,
339 pub tau_end_s: f64,
341 pub octave_count: usize,
343 pub point_count: usize,
345 pub mean_slope: f64,
347 pub coefficient: f64,
349}
350
351#[derive(Debug, Clone, PartialEq)]
353pub struct PowerLawNoiseFit {
354 pub dominant_per_octave: Vec<PowerLawOctave>,
356 pub coefficients: [f64; 5],
360 pub regions: Vec<PowerLawNoiseRegion>,
362}
363
364#[derive(Debug, Clone, PartialEq, Eq)]
366pub enum PowerLawNoiseError {
367 InvalidOptions {
369 field: &'static str,
371 reason: &'static str,
373 },
374 InvalidCurve {
376 curve: &'static str,
378 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
400pub enum AllanEstimator {
401 Adev,
403 OverlappingAdev,
405 Mdev,
407 Hdev,
409 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#[derive(Debug, Clone, PartialEq)]
427pub enum AllanError {
428 EmptySeries,
430 InvalidTau0 { tau0_s: f64 },
432 NoEstimators,
434 EmptyTauGrid,
436 InvalidAveragingFactor { averaging_factor: usize },
438 TooFewSamples {
440 estimator: AllanEstimator,
441 averaging_factor: usize,
442 available_phase_samples: usize,
443 },
444 NonFiniteSample { index: usize },
446 Gap { index: usize },
448 NonFiniteTau {
450 estimator: AllanEstimator,
451 averaging_factor: usize,
452 },
453 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
516pub 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
533pub 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
588pub 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
597pub 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
611pub 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
620pub 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
629pub 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
638pub 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, ®ions, 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}