use kshana::allan::{hadamard_adev, overlapping_adev};
const OADEV_ORACLE: &str = include_str!("fixtures/cs5071a/oadev_decade.txt");
const OHDEV_ORACLE: &str = include_str!("fixtures/cs5071a/ohdev_decade.txt");
struct OracleRow {
af: usize,
npts: u64,
min: f64,
sigma: f64,
max: f64,
}
fn parse_oracle(text: &str) -> Vec<OracleRow> {
let mut rows = Vec::new();
for line in text.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let c: Vec<&str> = line.split_whitespace().collect();
if c.len() < 7 {
continue;
}
rows.push(OracleRow {
af: c[0].parse().expect("AF"),
npts: c[2].parse().expect("# count"),
min: c[4].parse().expect("MinSigma"),
sigma: c[5].parse().expect("Sigma"),
max: c[6].parse().expect("MaxSigma"),
});
}
rows
}
fn phase_path() -> std::path::PathBuf {
if let Ok(p) = std::env::var("KSHANA_CS5071A_PATH") {
return std::path::PathBuf::from(p);
}
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("realdata-cache/cs5071a/5071A_phase.txt")
}
fn load_phase() -> Option<Vec<f64>> {
let path = phase_path();
let text = std::fs::read_to_string(&path).ok()?;
let phase: Vec<f64> = text
.lines()
.map(str::trim)
.filter(|l| !l.is_empty() && !l.starts_with('#'))
.map(|l| l.parse::<f64>().expect("phase sample"))
.collect();
Some(phase)
}
fn rel_err(got: f64, want: f64) -> f64 {
((got - want) / want).abs()
}
const TOL: f64 = 1e-3;
const WELL_SAMPLED: u64 = 100;
fn check_estimator(
name: &str,
oracle: &str,
phase: &[f64],
est: impl Fn(&[f64], f64, usize) -> f64,
) {
let rows = parse_oracle(oracle);
assert!(rows.len() >= 10, "{name}: oracle table looks truncated");
let mut checked = 0;
for r in &rows {
if r.af * 3 >= phase.len() {
continue;
}
let got = est(phase, 1.0, r.af);
if r.npts >= WELL_SAMPLED {
let rel = rel_err(got, r.sigma);
assert!(
rel < TOL,
"{name} AF={}: got {got:.6e}, Stable32 sigma {:.6e} (rel {rel:.2e} >= {TOL:.0e})",
r.af,
r.sigma
);
} else {
let lo = r.min * (1.0 - 1e-3);
let hi = r.max * (1.0 + 1e-3);
assert!(
got >= lo && got <= hi,
"{name} AF={}: got {got:.6e} outside Stable32 band [{:.6e}, {:.6e}]",
r.af,
r.min,
r.max
);
}
checked += 1;
}
assert!(
checked >= 10,
"{name}: only {checked} averaging factors checked"
);
eprintln!("[cs5071a] {name}: {checked} averaging factors match Stable32");
}
#[test]
fn cs5071a_real_caesium_estimators_match_stable32() {
let Some(phase) = load_phase() else {
eprintln!(
"[cs5071a] SKIP: raw phase data not found at {} \
(run scripts/fetch_cs5071a.sh or set KSHANA_CS5071A_PATH); CI stays green.",
phase_path().display()
);
return;
};
assert_eq!(
phase.len(),
556_990,
"expected the full 556 990-point 5071A series; got {}",
phase.len()
);
check_estimator("OADEV", OADEV_ORACLE, &phase, overlapping_adev);
check_estimator("OHDEV", OHDEV_ORACLE, &phase, hadamard_adev);
}
#[test]
fn cs5071a_oracle_tables_are_well_formed() {
for (name, txt) in [("oadev", OADEV_ORACLE), ("ohdev", OHDEV_ORACLE)] {
let rows = parse_oracle(txt);
assert!(rows.len() >= 15, "{name}: too few rows ({})", rows.len());
let first = &rows[0];
assert_eq!(first.af, 1, "{name}: first row should be AF=1");
assert!(
first.min <= first.sigma && first.sigma <= first.max,
"{name}: AF=1 band is not ordered"
);
for r in &rows {
assert!(
r.sigma > 0.0 && r.min > 0.0 && r.max > 0.0,
"{name}: nonpositive deviation"
);
assert!(r.min <= r.max, "{name}: AF={} band inverted", r.af);
}
}
}