use crate::estimation::mad_spread;
use crate::rinex::observations::RinexObs;
const SQRT_3: f64 = 1.732_050_807_568_877_2;
const DEFAULT_POWER_LAW_MIN_POINTS_PER_OCTAVE: usize = 2;
const DEFAULT_POWER_LAW_SLOPE_TOLERANCE: f64 = 0.125;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum AllanSeries<'a> {
PhaseSeconds(&'a [f64]),
FractionalFrequency(&'a [f64]),
PhaseSecondsWithGaps(&'a [Option<f64>]),
FractionalFrequencyWithGaps(&'a [Option<f64>]),
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum TauGrid {
#[default]
Octave,
All,
Explicit(Vec<usize>),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum GapPolicy {
#[default]
Reject,
OmitTerms,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AllanEstimatorSet {
pub adev: bool,
pub overlapping_adev: bool,
pub mdev: bool,
pub hdev: bool,
pub tdev: bool,
}
impl AllanEstimatorSet {
pub const fn none() -> Self {
Self {
adev: false,
overlapping_adev: false,
mdev: false,
hdev: false,
tdev: false,
}
}
pub const fn standard() -> Self {
Self {
adev: false,
overlapping_adev: true,
mdev: true,
hdev: true,
tdev: true,
}
}
pub const fn all() -> Self {
Self {
adev: true,
overlapping_adev: true,
mdev: true,
hdev: true,
tdev: true,
}
}
fn is_empty(self) -> bool {
!self.adev && !self.overlapping_adev && !self.mdev && !self.hdev && !self.tdev
}
}
impl Default for AllanEstimatorSet {
fn default() -> Self {
Self::standard()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct AllanOptions {
pub estimators: AllanEstimatorSet,
pub tau_grid: TauGrid,
pub gap_policy: GapPolicy,
}
#[derive(Debug, Clone, PartialEq)]
pub struct AllanInput<'a> {
pub series: AllanSeries<'a>,
pub tau0_s: f64,
pub options: AllanOptions,
}
#[derive(Debug, Clone, PartialEq)]
pub struct AllanResult {
pub tau_s: Vec<f64>,
pub deviation: Vec<f64>,
pub n: Vec<usize>,
}
impl AllanResult {
fn new() -> Self {
Self {
tau_s: Vec::new(),
deviation: Vec::new(),
n: Vec::new(),
}
}
fn push(&mut self, tau_s: f64, deviation: f64, n: usize) {
self.tau_s.push(tau_s);
self.deviation.push(deviation);
self.n.push(n);
}
pub fn len(&self) -> usize {
self.tau_s.len()
}
pub fn is_empty(&self) -> bool {
self.tau_s.is_empty()
}
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct AllanDeviationCurves {
pub adev: Option<AllanResult>,
pub overlapping_adev: Option<AllanResult>,
pub mdev: Option<AllanResult>,
pub hdev: Option<AllanResult>,
pub tdev: Option<AllanResult>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PowerLawNoiseType {
RandomWalkFM,
FlickerFM,
WhiteFM,
FlickerPM,
WhitePM,
}
impl PowerLawNoiseType {
pub const fn alpha(self) -> i32 {
match self {
Self::RandomWalkFM => -2,
Self::FlickerFM => -1,
Self::WhiteFM => 0,
Self::FlickerPM => 1,
Self::WhitePM => 2,
}
}
pub const fn coefficient_index(self) -> usize {
match self {
Self::RandomWalkFM => 0,
Self::FlickerFM => 1,
Self::WhiteFM => 2,
Self::FlickerPM => 3,
Self::WhitePM => 4,
}
}
}
pub fn allan_deviation_power_law_slope(noise_type: PowerLawNoiseType) -> f64 {
match noise_type {
PowerLawNoiseType::RandomWalkFM => 0.5,
PowerLawNoiseType::FlickerFM => 0.0,
PowerLawNoiseType::WhiteFM => -0.5,
PowerLawNoiseType::FlickerPM | PowerLawNoiseType::WhitePM => -1.0,
}
}
pub fn modified_allan_deviation_power_law_slope(noise_type: PowerLawNoiseType) -> f64 {
match noise_type {
PowerLawNoiseType::RandomWalkFM => 0.5,
PowerLawNoiseType::FlickerFM => 0.0,
PowerLawNoiseType::WhiteFM => -0.5,
PowerLawNoiseType::FlickerPM => -1.0,
PowerLawNoiseType::WhitePM => -1.5,
}
}
pub fn allan_variance_power_law_tau_exponent(noise_type: PowerLawNoiseType) -> i32 {
match noise_type {
PowerLawNoiseType::RandomWalkFM => 1,
PowerLawNoiseType::FlickerFM => 0,
PowerLawNoiseType::WhiteFM => -1,
PowerLawNoiseType::FlickerPM | PowerLawNoiseType::WhitePM => -2,
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct PowerLawNoiseOptions {
pub min_points_per_octave: usize,
pub slope_tolerance: f64,
pub scatter_tolerance: f64,
pub basic_tau_s: f64,
pub measurement_bandwidth_hz: f64,
}
impl PowerLawNoiseOptions {
pub const fn new(basic_tau_s: f64, measurement_bandwidth_hz: f64) -> Self {
Self {
min_points_per_octave: DEFAULT_POWER_LAW_MIN_POINTS_PER_OCTAVE,
slope_tolerance: DEFAULT_POWER_LAW_SLOPE_TOLERANCE,
scatter_tolerance: DEFAULT_POWER_LAW_SLOPE_TOLERANCE,
basic_tau_s,
measurement_bandwidth_hz,
}
}
pub fn sampled_at_nyquist(basic_tau_s: f64) -> Self {
Self::new(basic_tau_s, 0.5 / basic_tau_s)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PowerLawOctaveFlag {
UnderSampled,
DegenerateDeviation,
MissingModifiedAllan,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PowerLawOctaveDominance {
Dominant(PowerLawNoiseType),
Ambiguous,
Flagged(PowerLawOctaveFlag),
}
#[derive(Debug, Clone, PartialEq)]
pub struct PowerLawOctave {
pub tau_start_s: f64,
pub tau_end_s: f64,
pub point_count: usize,
pub adev_slope: Option<f64>,
pub mdev_slope: Option<f64>,
pub slope_scatter: Option<f64>,
pub dominance: PowerLawOctaveDominance,
}
#[derive(Debug, Clone, PartialEq)]
pub struct PowerLawNoiseRegion {
pub noise_type: PowerLawNoiseType,
pub tau_start_s: f64,
pub tau_end_s: f64,
pub octave_count: usize,
pub point_count: usize,
pub mean_slope: f64,
pub coefficient: f64,
}
#[derive(Debug, Clone, PartialEq)]
pub struct PowerLawNoiseFit {
pub dominant_per_octave: Vec<PowerLawOctave>,
pub coefficients: [f64; 5],
pub regions: Vec<PowerLawNoiseRegion>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PowerLawNoiseError {
InvalidOptions {
field: &'static str,
reason: &'static str,
},
InvalidCurve {
curve: &'static str,
reason: &'static str,
},
}
impl core::fmt::Display for PowerLawNoiseError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::InvalidOptions { field, reason } => {
write!(f, "invalid power-law option {field}: {reason}")
}
Self::InvalidCurve { curve, reason } => {
write!(f, "invalid {curve} curve: {reason}")
}
}
}
}
impl std::error::Error for PowerLawNoiseError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AllanEstimator {
Adev,
OverlappingAdev,
Mdev,
Hdev,
Tdev,
}
impl AllanEstimator {
fn label(self) -> &'static str {
match self {
Self::Adev => "ADEV",
Self::OverlappingAdev => "OADEV",
Self::Mdev => "MDEV",
Self::Hdev => "HDEV",
Self::Tdev => "TDEV",
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum AllanError {
EmptySeries,
InvalidTau0 { tau0_s: f64 },
NoEstimators,
EmptyTauGrid,
InvalidAveragingFactor { averaging_factor: usize },
TooFewSamples {
estimator: AllanEstimator,
averaging_factor: usize,
available_phase_samples: usize,
},
NonFiniteSample { index: usize },
Gap { index: usize },
NonFiniteTau {
estimator: AllanEstimator,
averaging_factor: usize,
},
NonFiniteDeviation {
estimator: AllanEstimator,
averaging_factor: usize,
},
}
impl core::fmt::Display for AllanError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::EmptySeries => write!(f, "Allan series is empty"),
Self::InvalidTau0 { tau0_s } => {
write!(f, "tau0_s must be finite and positive, got {tau0_s}")
}
Self::NoEstimators => write!(f, "no Allan estimators selected"),
Self::EmptyTauGrid => write!(f, "explicit tau grid is empty"),
Self::InvalidAveragingFactor { averaging_factor } => {
write!(
f,
"averaging factor must be positive, got {averaging_factor}"
)
}
Self::TooFewSamples {
estimator,
averaging_factor,
available_phase_samples,
} => write!(
f,
"{} has no valid terms for averaging factor {} with {} phase samples",
estimator.label(),
averaging_factor,
available_phase_samples
),
Self::NonFiniteSample { index } => {
write!(f, "sample {index} is not finite")
}
Self::Gap { index } => {
write!(f, "sample {index} is missing")
}
Self::NonFiniteTau {
estimator,
averaging_factor,
} => write!(
f,
"{} tau is not finite for averaging factor {}",
estimator.label(),
averaging_factor
),
Self::NonFiniteDeviation {
estimator,
averaging_factor,
} => write!(
f,
"{} deviation is not finite for averaging factor {}",
estimator.label(),
averaging_factor
),
}
}
}
impl std::error::Error for AllanError {}
pub fn receiver_clock_phase_deviations(obs: &RinexObs) -> Vec<Option<f64>> {
obs.epochs()
.iter()
.map(|epoch| {
if epoch.flag > 1 {
None
} else {
epoch.rcv_clock_offset_s
}
})
.collect()
}
pub fn compute_allan_deviations(
input: &AllanInput<'_>,
) -> Result<AllanDeviationCurves, AllanError> {
if input.options.estimators.is_empty() {
return Err(AllanError::NoEstimators);
}
let phase = prepare_phase(input.series, input.tau0_s, input.options.gap_policy)?;
let mut curves = AllanDeviationCurves::default();
if input.options.estimators.adev {
curves.adev = Some(compute_curve(
&phase,
input.tau0_s,
&input.options.tau_grid,
AllanEstimator::Adev,
)?);
}
if input.options.estimators.overlapping_adev {
curves.overlapping_adev = Some(compute_curve(
&phase,
input.tau0_s,
&input.options.tau_grid,
AllanEstimator::OverlappingAdev,
)?);
}
if input.options.estimators.mdev {
curves.mdev = Some(compute_curve(
&phase,
input.tau0_s,
&input.options.tau_grid,
AllanEstimator::Mdev,
)?);
}
if input.options.estimators.hdev {
curves.hdev = Some(compute_curve(
&phase,
input.tau0_s,
&input.options.tau_grid,
AllanEstimator::Hdev,
)?);
}
if input.options.estimators.tdev {
curves.tdev = Some(compute_curve(
&phase,
input.tau0_s,
&input.options.tau_grid,
AllanEstimator::Tdev,
)?);
}
Ok(curves)
}
pub fn allan_deviation(
series: AllanSeries<'_>,
tau0_s: f64,
averaging_factors: &[usize],
) -> Result<AllanResult, AllanError> {
compute_explicit(series, tau0_s, averaging_factors, AllanEstimator::Adev)
}
pub fn overlapping_adev(
series: AllanSeries<'_>,
tau0_s: f64,
averaging_factors: &[usize],
) -> Result<AllanResult, AllanError> {
compute_explicit(
series,
tau0_s,
averaging_factors,
AllanEstimator::OverlappingAdev,
)
}
pub fn modified_adev(
series: AllanSeries<'_>,
tau0_s: f64,
averaging_factors: &[usize],
) -> Result<AllanResult, AllanError> {
compute_explicit(series, tau0_s, averaging_factors, AllanEstimator::Mdev)
}
pub fn hadamard_deviation(
series: AllanSeries<'_>,
tau0_s: f64,
averaging_factors: &[usize],
) -> Result<AllanResult, AllanError> {
compute_explicit(series, tau0_s, averaging_factors, AllanEstimator::Hdev)
}
pub fn time_deviation(
series: AllanSeries<'_>,
tau0_s: f64,
averaging_factors: &[usize],
) -> Result<AllanResult, AllanError> {
compute_explicit(series, tau0_s, averaging_factors, AllanEstimator::Tdev)
}
pub fn fit_power_law_noise(
adev: &AllanResult,
mdev: &AllanResult,
options: PowerLawNoiseOptions,
) -> Result<PowerLawNoiseFit, PowerLawNoiseError> {
validate_power_law_options(options)?;
validate_power_law_curve("ADEV", adev)?;
validate_power_law_curve("MDEV", mdev)?;
let dominant_per_octave = classify_power_law_octaves(adev, mdev, options);
let regions = build_power_law_regions(&dominant_per_octave, adev, mdev, options);
let mut coefficients = [f64::NAN; 5];
for noise_type in POWER_LAW_NOISE_TYPES {
let coefficient = fit_coefficient_for_type(noise_type, ®ions, adev, mdev, options);
if let Some(coefficient) = coefficient {
coefficients[noise_type.coefficient_index()] = coefficient;
}
}
Ok(PowerLawNoiseFit {
dominant_per_octave,
coefficients,
regions,
})
}
fn compute_explicit(
series: AllanSeries<'_>,
tau0_s: f64,
averaging_factors: &[usize],
estimator: AllanEstimator,
) -> Result<AllanResult, AllanError> {
let phase = prepare_phase(series, tau0_s, GapPolicy::Reject)?;
compute_curve(
&phase,
tau0_s,
&TauGrid::Explicit(averaging_factors.to_vec()),
estimator,
)
}
const POWER_LAW_NOISE_TYPES: [PowerLawNoiseType; 5] = [
PowerLawNoiseType::RandomWalkFM,
PowerLawNoiseType::FlickerFM,
PowerLawNoiseType::WhiteFM,
PowerLawNoiseType::FlickerPM,
PowerLawNoiseType::WhitePM,
];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum AdevSlopeClass {
Noise(PowerLawNoiseType),
PhaseModulation,
Ambiguous,
}
fn validate_power_law_options(options: PowerLawNoiseOptions) -> Result<(), PowerLawNoiseError> {
if options.min_points_per_octave < 2 {
return Err(PowerLawNoiseError::InvalidOptions {
field: "min_points_per_octave",
reason: "must be at least 2",
});
}
if !(options.slope_tolerance.is_finite() && options.slope_tolerance > 0.0) {
return Err(PowerLawNoiseError::InvalidOptions {
field: "slope_tolerance",
reason: "must be finite and positive",
});
}
if !(options.scatter_tolerance.is_finite() && options.scatter_tolerance >= 0.0) {
return Err(PowerLawNoiseError::InvalidOptions {
field: "scatter_tolerance",
reason: "must be finite and nonnegative",
});
}
if !(options.basic_tau_s.is_finite() && options.basic_tau_s > 0.0) {
return Err(PowerLawNoiseError::InvalidOptions {
field: "basic_tau_s",
reason: "must be finite and positive",
});
}
if !(options.measurement_bandwidth_hz.is_finite() && options.measurement_bandwidth_hz > 0.0) {
return Err(PowerLawNoiseError::InvalidOptions {
field: "measurement_bandwidth_hz",
reason: "must be finite and positive",
});
}
Ok(())
}
fn validate_power_law_curve(
curve_name: &'static str,
curve: &AllanResult,
) -> Result<(), PowerLawNoiseError> {
if curve.tau_s.len() != curve.deviation.len() || curve.tau_s.len() != curve.n.len() {
return Err(PowerLawNoiseError::InvalidCurve {
curve: curve_name,
reason: "tau, deviation, and term-count lengths must match",
});
}
let mut previous_tau_s = None;
for (index, (&tau_s, &deviation)) in curve.tau_s.iter().zip(curve.deviation.iter()).enumerate()
{
if !(tau_s.is_finite() && tau_s > 0.0) {
return Err(PowerLawNoiseError::InvalidCurve {
curve: curve_name,
reason: "tau values must be finite and positive",
});
}
if previous_tau_s.is_some_and(|previous| tau_s <= previous) {
return Err(PowerLawNoiseError::InvalidCurve {
curve: curve_name,
reason: "tau values must be strictly increasing",
});
}
previous_tau_s = Some(tau_s);
if !(deviation.is_finite() && deviation >= 0.0) {
return Err(PowerLawNoiseError::InvalidCurve {
curve: curve_name,
reason: "deviations must be finite and nonnegative",
});
}
if curve.n[index] == 0 {
return Err(PowerLawNoiseError::InvalidCurve {
curve: curve_name,
reason: "term counts must be positive",
});
}
}
Ok(())
}
fn classify_power_law_octaves(
adev: &AllanResult,
mdev: &AllanResult,
options: PowerLawNoiseOptions,
) -> Vec<PowerLawOctave> {
let mut octaves = Vec::new();
let mut start = 0usize;
while start < adev.tau_s.len() {
let end = octave_end_index(&adev.tau_s, start);
let point_count = end + 1 - start;
if point_count < options.min_points_per_octave {
let tau_start_s = adev.tau_s[start];
octaves.push(PowerLawOctave {
tau_start_s,
tau_end_s: tau_start_s,
point_count,
adev_slope: None,
mdev_slope: None,
slope_scatter: None,
dominance: PowerLawOctaveDominance::Flagged(PowerLawOctaveFlag::UnderSampled),
});
start += 1;
continue;
}
octaves.push(classify_power_law_window(start, end, adev, mdev, options));
if end >= adev.tau_s.len() - 1 {
break;
}
start = end;
}
octaves
}
fn octave_end_index(tau_s: &[f64], start: usize) -> usize {
let tau_limit_s = tau_s[start] * 2.0;
let tolerance = tau_limit_s.abs().max(1.0) * 32.0 * f64::EPSILON;
let mut end = start;
while end + 1 < tau_s.len() && tau_s[end + 1] <= tau_limit_s + tolerance {
end += 1;
}
end
}
fn classify_power_law_window(
start: usize,
end: usize,
adev: &AllanResult,
mdev: &AllanResult,
options: PowerLawNoiseOptions,
) -> PowerLawOctave {
let tau_start_s = adev.tau_s[start];
let tau_end_s = adev.tau_s[end];
let point_count = end + 1 - start;
let Some(adev_slope) = log_log_slope_for_curve(adev, start, end) else {
return PowerLawOctave {
tau_start_s,
tau_end_s,
point_count,
adev_slope: None,
mdev_slope: None,
slope_scatter: None,
dominance: PowerLawOctaveDominance::Flagged(PowerLawOctaveFlag::DegenerateDeviation),
};
};
let adjacent_slopes = adjacent_log_log_slopes(adev, start, end);
let slope_scatter = robust_slope_scatter(&adjacent_slopes);
let scatter_is_ambiguous =
slope_scatter.is_some_and(|scatter| scatter > options.scatter_tolerance);
let adev_class = classify_adev_slope(adev_slope, options.slope_tolerance);
let local_adev_consistent =
local_adev_slopes_consistent(&adjacent_slopes, adev_class, options.slope_tolerance);
let (mdev_slope, mdev_scatter, local_mdev_consistent) =
mdev_slope_in_range(mdev, tau_start_s, tau_end_s, options);
let dominance = if scatter_is_ambiguous || !local_adev_consistent {
PowerLawOctaveDominance::Ambiguous
} else {
match adev_class {
AdevSlopeClass::Noise(noise_type) => {
if let Some(mdev_slope) = mdev_slope {
match classify_mdev_slope(mdev_slope, options.slope_tolerance) {
Some(mdev_type) if mdev_type != noise_type => {
PowerLawOctaveDominance::Ambiguous
}
None => PowerLawOctaveDominance::Ambiguous,
Some(_) => PowerLawOctaveDominance::Dominant(noise_type),
}
} else {
PowerLawOctaveDominance::Dominant(noise_type)
}
}
AdevSlopeClass::PhaseModulation => match mdev_slope {
Some(mdev_slope)
if !mdev_scatter.is_some_and(|scatter| scatter > options.scatter_tolerance)
&& local_mdev_consistent =>
{
match classify_mdev_slope(mdev_slope, options.slope_tolerance) {
Some(PowerLawNoiseType::FlickerPM) => {
PowerLawOctaveDominance::Dominant(PowerLawNoiseType::FlickerPM)
}
Some(PowerLawNoiseType::WhitePM) => {
PowerLawOctaveDominance::Dominant(PowerLawNoiseType::WhitePM)
}
_ => PowerLawOctaveDominance::Ambiguous,
}
}
Some(_) => PowerLawOctaveDominance::Ambiguous,
None => PowerLawOctaveDominance::Flagged(PowerLawOctaveFlag::MissingModifiedAllan),
},
AdevSlopeClass::Ambiguous => PowerLawOctaveDominance::Ambiguous,
}
};
PowerLawOctave {
tau_start_s,
tau_end_s,
point_count,
adev_slope: Some(adev_slope),
mdev_slope,
slope_scatter,
dominance,
}
}
fn log_log_slope_for_curve(curve: &AllanResult, start: usize, end: usize) -> Option<f64> {
if end <= start {
return None;
}
let tau = &curve.tau_s[start..=end];
let deviation = &curve.deviation[start..=end];
if deviation.iter().any(|&value| value <= 0.0) {
return None;
}
let n = tau.len() as f64;
let (sum_x, sum_y) = tau
.iter()
.zip(deviation.iter())
.fold((0.0, 0.0), |(sum_x, sum_y), (&tau_s, &sigma)| {
(sum_x + tau_s.ln(), sum_y + sigma.ln())
});
let mean_x = sum_x / n;
let mean_y = sum_y / n;
let (num, den) =
tau.iter()
.zip(deviation.iter())
.fold((0.0, 0.0), |(num, den), (&tau_s, &sigma)| {
let dx = tau_s.ln() - mean_x;
let dy = sigma.ln() - mean_y;
(num + dx * dy, den + dx * dx)
});
if den > 0.0 {
Some(num / den)
} else {
None
}
}
fn adjacent_log_log_slopes(curve: &AllanResult, start: usize, end: usize) -> Vec<f64> {
let mut slopes = Vec::new();
for index in start..end {
let tau0 = curve.tau_s[index];
let tau1 = curve.tau_s[index + 1];
let sigma0 = curve.deviation[index];
let sigma1 = curve.deviation[index + 1];
if sigma0 <= 0.0 || sigma1 <= 0.0 {
continue;
}
let denominator = tau1.ln() - tau0.ln();
if denominator > 0.0 {
slopes.push((sigma1.ln() - sigma0.ln()) / denominator);
}
}
slopes
}
fn robust_slope_scatter(slopes: &[f64]) -> Option<f64> {
if slopes.len() < 2 {
return Some(0.0);
}
mad_spread(slopes, 0.0).ok()
}
fn local_adev_slopes_consistent(slopes: &[f64], expected: AdevSlopeClass, tolerance: f64) -> bool {
slopes
.iter()
.all(|&slope| classify_adev_slope(slope, tolerance) == expected)
}
fn local_mdev_slopes_consistent(
slopes: &[f64],
expected: Option<PowerLawNoiseType>,
tolerance: f64,
) -> bool {
match expected {
Some(expected) => slopes
.iter()
.all(|&slope| classify_mdev_slope(slope, tolerance) == Some(expected)),
None => true,
}
}
fn mdev_slope_in_range(
mdev: &AllanResult,
tau_start_s: f64,
tau_end_s: f64,
options: PowerLawNoiseOptions,
) -> (Option<f64>, Option<f64>, bool) {
let indices = curve_indices_in_range(mdev, tau_start_s, tau_end_s);
if indices.len() < options.min_points_per_octave {
return (None, None, false);
}
let start = indices[0];
let end = *indices.last().expect("nonempty indices");
let Some(slope) = log_log_slope_for_curve(mdev, start, end) else {
return (None, None, false);
};
let adjacent = adjacent_log_log_slopes(mdev, start, end);
let classified = classify_mdev_slope(slope, options.slope_tolerance);
let consistent = local_mdev_slopes_consistent(&adjacent, classified, options.slope_tolerance);
(Some(slope), robust_slope_scatter(&adjacent), consistent)
}
fn curve_indices_in_range(curve: &AllanResult, tau_start_s: f64, tau_end_s: f64) -> Vec<usize> {
let tolerance = tau_end_s.abs().max(tau_start_s.abs()).max(1.0) * 32.0 * f64::EPSILON;
curve
.tau_s
.iter()
.enumerate()
.filter_map(|(index, &tau_s)| {
if tau_s + tolerance >= tau_start_s && tau_s <= tau_end_s + tolerance {
Some(index)
} else {
None
}
})
.collect()
}
fn classify_adev_slope(slope: f64, tolerance: f64) -> AdevSlopeClass {
let phase_target = -1.0;
if (slope - phase_target).abs() <= tolerance {
return AdevSlopeClass::PhaseModulation;
}
let candidates = [
PowerLawNoiseType::RandomWalkFM,
PowerLawNoiseType::FlickerFM,
PowerLawNoiseType::WhiteFM,
];
let mut best = None;
let mut best_distance = f64::INFINITY;
for noise_type in candidates {
let distance = (slope - allan_deviation_power_law_slope(noise_type)).abs();
if distance < best_distance {
best = Some(noise_type);
best_distance = distance;
}
}
if best_distance <= tolerance {
AdevSlopeClass::Noise(best.expect("candidate selected"))
} else {
AdevSlopeClass::Ambiguous
}
}
fn classify_mdev_slope(slope: f64, tolerance: f64) -> Option<PowerLawNoiseType> {
let mut best = None;
let mut best_distance = f64::INFINITY;
for noise_type in POWER_LAW_NOISE_TYPES {
let distance = (slope - modified_allan_deviation_power_law_slope(noise_type)).abs();
if distance < best_distance {
best = Some(noise_type);
best_distance = distance;
}
}
if best_distance <= tolerance {
best
} else {
None
}
}
fn build_power_law_regions(
octaves: &[PowerLawOctave],
adev: &AllanResult,
mdev: &AllanResult,
options: PowerLawNoiseOptions,
) -> Vec<PowerLawNoiseRegion> {
let mut regions = Vec::new();
let mut current: Option<PowerLawNoiseRegion> = None;
for octave in octaves {
let PowerLawOctaveDominance::Dominant(noise_type) = octave.dominance else {
if let Some(region) = current.take() {
regions.push(region);
}
continue;
};
let Some(slope) = octave_slope_for_region(noise_type, octave) else {
continue;
};
if current
.as_ref()
.is_some_and(|region| region.noise_type == noise_type)
{
let region = current.as_mut().expect("current region");
let next_count = region.octave_count + 1;
region.tau_end_s = octave.tau_end_s;
region.mean_slope =
(region.mean_slope * region.octave_count as f64 + slope) / next_count as f64;
region.octave_count = next_count;
} else {
if let Some(region) = current.take() {
regions.push(region);
}
current = Some(PowerLawNoiseRegion {
noise_type,
tau_start_s: octave.tau_start_s,
tau_end_s: octave.tau_end_s,
octave_count: 1,
point_count: 0,
mean_slope: slope,
coefficient: f64::NAN,
});
}
}
if let Some(region) = current {
regions.push(region);
}
for region in &mut regions {
region.point_count = count_points_for_region(region.noise_type, region, adev, mdev);
if let Some(coefficient) = fit_coefficient_for_ranges(
region.noise_type,
&[(region.tau_start_s, region.tau_end_s)],
adev,
mdev,
options,
) {
region.coefficient = coefficient;
}
}
regions
}
fn octave_slope_for_region(noise_type: PowerLawNoiseType, octave: &PowerLawOctave) -> Option<f64> {
match noise_type {
PowerLawNoiseType::FlickerPM | PowerLawNoiseType::WhitePM => octave.mdev_slope,
_ => octave.adev_slope,
}
}
fn count_points_for_region(
noise_type: PowerLawNoiseType,
region: &PowerLawNoiseRegion,
adev: &AllanResult,
mdev: &AllanResult,
) -> usize {
let curve = coefficient_curve(noise_type, adev, mdev);
curve_indices_in_range(curve, region.tau_start_s, region.tau_end_s).len()
}
fn fit_coefficient_for_type(
noise_type: PowerLawNoiseType,
regions: &[PowerLawNoiseRegion],
adev: &AllanResult,
mdev: &AllanResult,
options: PowerLawNoiseOptions,
) -> Option<f64> {
let ranges = regions
.iter()
.filter(|region| region.noise_type == noise_type)
.map(|region| (region.tau_start_s, region.tau_end_s))
.collect::<Vec<_>>();
fit_coefficient_for_ranges(noise_type, &ranges, adev, mdev, options)
}
fn fit_coefficient_for_ranges(
noise_type: PowerLawNoiseType,
ranges: &[(f64, f64)],
adev: &AllanResult,
mdev: &AllanResult,
options: PowerLawNoiseOptions,
) -> Option<f64> {
if ranges.is_empty() {
return None;
}
let curve = coefficient_curve(noise_type, adev, mdev);
let mut sum_ab = 0.0;
let mut sum_aa = 0.0;
for (index, &tau_s) in curve.tau_s.iter().enumerate() {
if !ranges
.iter()
.any(|&(start, end)| tau_in_range(tau_s, start, end))
{
continue;
}
let factor = power_law_variance_factor(noise_type, tau_s, options)?;
if !(factor.is_finite() && factor > 0.0) {
return None;
}
let variance = curve.deviation[index] * curve.deviation[index];
if !(variance.is_finite() && variance >= 0.0) {
return None;
}
sum_ab += factor * variance;
sum_aa += factor * factor;
}
if sum_aa > 0.0 {
Some(sum_ab / sum_aa)
} else {
None
}
}
fn coefficient_curve<'a>(
noise_type: PowerLawNoiseType,
adev: &'a AllanResult,
mdev: &'a AllanResult,
) -> &'a AllanResult {
match noise_type {
PowerLawNoiseType::FlickerPM | PowerLawNoiseType::WhitePM => mdev,
_ => adev,
}
}
fn tau_in_range(tau_s: f64, tau_start_s: f64, tau_end_s: f64) -> bool {
let tolerance = tau_end_s.abs().max(tau_start_s.abs()).max(1.0) * 32.0 * f64::EPSILON;
tau_s + tolerance >= tau_start_s && tau_s <= tau_end_s + tolerance
}
fn power_law_variance_factor(
noise_type: PowerLawNoiseType,
tau_s: f64,
options: PowerLawNoiseOptions,
) -> Option<f64> {
if !(tau_s.is_finite() && tau_s > 0.0) {
return None;
}
let pi = core::f64::consts::PI;
let factor = match noise_type {
PowerLawNoiseType::RandomWalkFM => (2.0 * pi * pi / 3.0) * tau_s,
PowerLawNoiseType::FlickerFM => 2.0 * core::f64::consts::LN_2,
PowerLawNoiseType::WhiteFM => 0.5 / tau_s,
PowerLawNoiseType::FlickerPM => {
let numerator = 3.0 * (256.0_f64 / 27.0).ln();
numerator / (8.0 * pi * pi * tau_s * tau_s)
}
PowerLawNoiseType::WhitePM => {
let numerator = 3.0 * options.measurement_bandwidth_hz * options.basic_tau_s;
numerator / (4.0 * pi * pi * tau_s * tau_s * tau_s)
}
};
if factor.is_finite() && factor > 0.0 {
Some(factor)
} else {
None
}
}
#[derive(Debug, Clone, Copy)]
struct PhasePoint {
value_s: f64,
run_id: usize,
}
fn prepare_phase(
series: AllanSeries<'_>,
tau0_s: f64,
gap_policy: GapPolicy,
) -> Result<Vec<Option<PhasePoint>>, AllanError> {
validate_tau0(tau0_s)?;
match series {
AllanSeries::PhaseSeconds(values) => phase_from_contiguous(values),
AllanSeries::FractionalFrequency(values) => phase_from_contiguous_frequency(values, tau0_s),
AllanSeries::PhaseSecondsWithGaps(values) => phase_from_gapped(values, gap_policy),
AllanSeries::FractionalFrequencyWithGaps(values) => {
phase_from_gapped_frequency(values, tau0_s, gap_policy)
}
}
}
fn validate_tau0(tau0_s: f64) -> Result<(), AllanError> {
if tau0_s.is_finite() && tau0_s > 0.0 {
Ok(())
} else {
Err(AllanError::InvalidTau0 { tau0_s })
}
}
fn phase_from_contiguous(values: &[f64]) -> Result<Vec<Option<PhasePoint>>, AllanError> {
if values.is_empty() {
return Err(AllanError::EmptySeries);
}
values
.iter()
.enumerate()
.map(|(index, &value_s)| {
validate_sample(index, value_s).map(|value_s| Some(PhasePoint { value_s, run_id: 0 }))
})
.collect()
}
fn phase_from_contiguous_frequency(
values: &[f64],
tau0_s: f64,
) -> Result<Vec<Option<PhasePoint>>, AllanError> {
if values.is_empty() {
return Err(AllanError::EmptySeries);
}
let mut phase = Vec::with_capacity(values.len() + 1);
let mut value_s = 0.0;
phase.push(Some(PhasePoint { value_s, run_id: 0 }));
for (index, &frequency) in values.iter().enumerate() {
let frequency = validate_sample(index, frequency)?;
value_s += tau0_s * frequency;
if !value_s.is_finite() {
return Err(AllanError::NonFiniteSample { index });
}
phase.push(Some(PhasePoint { value_s, run_id: 0 }));
}
Ok(phase)
}
fn phase_from_gapped(
values: &[Option<f64>],
gap_policy: GapPolicy,
) -> Result<Vec<Option<PhasePoint>>, AllanError> {
if values.is_empty() {
return Err(AllanError::EmptySeries);
}
let mut phase = Vec::with_capacity(values.len());
let mut run_id = 0usize;
let mut in_run = false;
for (index, value) in values.iter().enumerate() {
match value {
Some(value_s) => {
let value_s = validate_sample(index, *value_s)?;
if !in_run {
run_id += 1;
in_run = true;
}
phase.push(Some(PhasePoint { value_s, run_id }));
}
None => {
if gap_policy == GapPolicy::Reject {
return Err(AllanError::Gap { index });
}
in_run = false;
phase.push(None);
}
}
}
Ok(phase)
}
fn phase_from_gapped_frequency(
values: &[Option<f64>],
tau0_s: f64,
gap_policy: GapPolicy,
) -> Result<Vec<Option<PhasePoint>>, AllanError> {
if values.is_empty() {
return Err(AllanError::EmptySeries);
}
if gap_policy == GapPolicy::Reject {
for (index, value) in values.iter().enumerate() {
if value.is_none() {
return Err(AllanError::Gap { index });
}
}
}
let mut phase = vec![None; values.len() + 1];
let mut run_id = 0usize;
let mut index = 0usize;
while index < values.len() {
if values[index].is_none() {
index += 1;
continue;
};
run_id += 1;
let mut value_s = 0.0;
phase[index] = Some(PhasePoint { value_s, run_id });
let mut sample_index = index;
while let Some(current) = values.get(sample_index).copied().flatten() {
let current = validate_sample(sample_index, current)?;
value_s += tau0_s * current;
if !value_s.is_finite() {
return Err(AllanError::NonFiniteSample {
index: sample_index,
});
}
phase[sample_index + 1] = Some(PhasePoint { value_s, run_id });
sample_index += 1;
}
index = sample_index + 1;
}
Ok(phase)
}
fn validate_sample(index: usize, value: f64) -> Result<f64, AllanError> {
if value.is_finite() {
Ok(value)
} else {
Err(AllanError::NonFiniteSample { index })
}
}
fn compute_curve(
phase: &[Option<PhasePoint>],
tau0_s: f64,
tau_grid: &TauGrid,
estimator: AllanEstimator,
) -> Result<AllanResult, AllanError> {
let averaging_factors = averaging_factors(phase.len(), estimator, tau_grid)?;
let strict = matches!(tau_grid, TauGrid::Explicit(_));
let mut result = AllanResult::new();
for m in averaging_factors {
if m == 0 {
return Err(AllanError::InvalidAveragingFactor {
averaging_factor: m,
});
}
if candidate_count(phase.len(), estimator, m).is_none() {
if strict {
return Err(AllanError::TooFewSamples {
estimator,
averaging_factor: m,
available_phase_samples: phase.len(),
});
}
continue;
}
let tau_s = tau0_s * m as f64;
if !tau_s.is_finite() {
return Err(AllanError::NonFiniteTau {
estimator,
averaging_factor: m,
});
}
let (sum_sq, n) = estimator_sum_squares(phase, estimator, m);
if n == 0 {
if strict {
return Err(AllanError::TooFewSamples {
estimator,
averaging_factor: m,
available_phase_samples: phase.len(),
});
}
continue;
}
let deviation = deviation_from_sum(sum_sq, n, tau_s, m, estimator);
if !deviation.is_finite() {
return Err(AllanError::NonFiniteDeviation {
estimator,
averaging_factor: m,
});
}
result.push(tau_s, deviation, n);
}
if result.is_empty() {
return Err(AllanError::TooFewSamples {
estimator,
averaging_factor: 1,
available_phase_samples: phase.len(),
});
}
Ok(result)
}
fn averaging_factors(
phase_len: usize,
estimator: AllanEstimator,
tau_grid: &TauGrid,
) -> Result<Vec<usize>, AllanError> {
match tau_grid {
TauGrid::Explicit(values) => {
if values.is_empty() {
Err(AllanError::EmptyTauGrid)
} else {
Ok(values.clone())
}
}
TauGrid::All => Ok((1..=max_averaging_factor(phase_len, estimator)).collect()),
TauGrid::Octave => {
let max_m = max_averaging_factor(phase_len, estimator);
let mut values = Vec::new();
let mut m = 1usize;
while m <= max_m {
values.push(m);
if m > max_m / 2 {
break;
}
m *= 2;
}
Ok(values)
}
}
}
fn max_averaging_factor(phase_len: usize, estimator: AllanEstimator) -> usize {
match estimator {
AllanEstimator::Adev | AllanEstimator::OverlappingAdev => phase_len.saturating_sub(1) / 2,
AllanEstimator::Mdev | AllanEstimator::Tdev => phase_len / 3,
AllanEstimator::Hdev => phase_len.saturating_sub(1) / 3,
}
}
fn candidate_count(phase_len: usize, estimator: AllanEstimator, m: usize) -> Option<usize> {
if m == 0 {
return None;
}
match estimator {
AllanEstimator::Adev => {
let frequency_len = phase_len.checked_sub(1)?;
(frequency_len / m).checked_sub(1)
}
AllanEstimator::OverlappingAdev => phase_len.checked_sub(m.checked_mul(2)?),
AllanEstimator::Mdev | AllanEstimator::Tdev => {
phase_len.checked_sub(m.checked_mul(3)?)?.checked_add(1)
}
AllanEstimator::Hdev => phase_len.checked_sub(m.checked_mul(3)?),
}
}
fn estimator_sum_squares(
phase: &[Option<PhasePoint>],
estimator: AllanEstimator,
m: usize,
) -> (f64, usize) {
match estimator {
AllanEstimator::Adev => plain_adev_sum_squares(phase, m),
AllanEstimator::OverlappingAdev => overlapping_adev_sum_squares(phase, m),
AllanEstimator::Mdev | AllanEstimator::Tdev => modified_adev_sum_squares(phase, m),
AllanEstimator::Hdev => hadamard_sum_squares(phase, m),
}
}
fn deviation_from_sum(
sum_sq: f64,
n: usize,
tau_s: f64,
m: usize,
estimator: AllanEstimator,
) -> f64 {
let n = n as f64;
match estimator {
AllanEstimator::Adev | AllanEstimator::OverlappingAdev => {
(sum_sq / (2.0 * n * tau_s * tau_s)).sqrt()
}
AllanEstimator::Mdev => {
let mf = m as f64;
(sum_sq / (2.0 * mf * mf * n * tau_s * tau_s)).sqrt()
}
AllanEstimator::Hdev => (sum_sq / (6.0 * n * tau_s * tau_s)).sqrt(),
AllanEstimator::Tdev => {
let mf = m as f64;
let mdev = (sum_sq / (2.0 * mf * mf * n * tau_s * tau_s)).sqrt();
tau_s * mdev / SQRT_3
}
}
}
fn plain_adev_sum_squares(phase: &[Option<PhasePoint>], m: usize) -> (f64, usize) {
let Some(count) = candidate_count(phase.len(), AllanEstimator::Adev, m) else {
return (0.0, 0);
};
let mut sum_sq = 0.0;
let mut n = 0usize;
for j in 0..count {
let i = j * m;
if let Some((diff, _)) = second_difference(phase, i, m) {
let square = diff * diff;
sum_sq += square;
n += 1;
}
}
(sum_sq, n)
}
fn overlapping_adev_sum_squares(phase: &[Option<PhasePoint>], m: usize) -> (f64, usize) {
let Some(count) = candidate_count(phase.len(), AllanEstimator::OverlappingAdev, m) else {
return (0.0, 0);
};
let mut sum_sq = 0.0;
let mut n = 0usize;
for i in 0..count {
if let Some((diff, _)) = second_difference(phase, i, m) {
let square = diff * diff;
sum_sq += square;
n += 1;
}
}
(sum_sq, n)
}
fn modified_adev_sum_squares(phase: &[Option<PhasePoint>], m: usize) -> (f64, usize) {
let Some(count) = candidate_count(phase.len(), AllanEstimator::Mdev, m) else {
return (0.0, 0);
};
let mut sum_sq = 0.0;
let mut n = 0usize;
for j in 0..count {
let mut inner = 0.0;
let mut run_id = None;
let mut valid = true;
for i in j..(j + m) {
let Some((diff, diff_run_id)) = second_difference(phase, i, m) else {
valid = false;
break;
};
if run_id.is_some_and(|current| current != diff_run_id) {
valid = false;
break;
}
run_id = Some(diff_run_id);
inner += diff;
}
if valid {
let square = inner * inner;
sum_sq += square;
n += 1;
}
}
(sum_sq, n)
}
fn hadamard_sum_squares(phase: &[Option<PhasePoint>], m: usize) -> (f64, usize) {
let Some(count) = candidate_count(phase.len(), AllanEstimator::Hdev, m) else {
return (0.0, 0);
};
let mut sum_sq = 0.0;
let mut n = 0usize;
for i in 0..count {
if let Some(diff) = third_difference(phase, i, m) {
let square = diff * diff;
sum_sq += square;
n += 1;
}
}
(sum_sq, n)
}
fn second_difference(phase: &[Option<PhasePoint>], i: usize, m: usize) -> Option<(f64, usize)> {
let x0 = phase.get(i).copied().flatten()?;
let x1 = phase.get(i + m).copied().flatten()?;
let x2 = phase.get(i + 2 * m).copied().flatten()?;
if x0.run_id != x1.run_id || x0.run_id != x2.run_id {
return None;
}
Some((x2.value_s - 2.0 * x1.value_s + x0.value_s, x0.run_id))
}
fn third_difference(phase: &[Option<PhasePoint>], i: usize, m: usize) -> Option<f64> {
let x0 = phase.get(i).copied().flatten()?;
let x1 = phase.get(i + m).copied().flatten()?;
let x2 = phase.get(i + 2 * m).copied().flatten()?;
let x3 = phase.get(i + 3 * m).copied().flatten()?;
if x0.run_id != x1.run_id || x0.run_id != x2.run_id || x0.run_id != x3.run_id {
return None;
}
Some(x3.value_s - 3.0 * x2.value_s + 3.0 * x1.value_s - x0.value_s)
}