#[derive(Clone, Debug, PartialEq)]
pub struct PhaseSeries {
pub t0: f64,
pub tau0: f64,
pub x: Vec<f64>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct GriddingLog {
pub off_grid: usize,
pub duplicates: usize,
pub non_finite: usize,
}
impl PhaseSeries {
pub fn from_samples(samples: &[(f64, f64)], tau0: f64) -> (PhaseSeries, GriddingLog) {
let mut log = GriddingLog::default();
let finite: Vec<(f64, f64)> = samples
.iter()
.copied()
.filter(|(t, x)| {
let ok = t.is_finite() && x.is_finite();
if !ok {
log.non_finite += 1;
}
ok
})
.collect();
let Some(t0) = finite.iter().map(|s| s.0).reduce(f64::min) else {
return (
PhaseSeries {
t0: 0.0,
tau0,
x: Vec::new(),
},
log,
);
};
let t1 = finite.iter().map(|s| s.0).fold(t0, f64::max);
let n = ((t1 - t0) / tau0).round() as usize + 1;
let mut x = vec![f64::NAN; n];
for (t, v) in finite {
let k = (t - t0) / tau0;
let i = k.round();
if (k - i).abs() > 0.01 {
log.off_grid += 1;
continue;
}
let i = i as usize;
if x[i].is_nan() {
x[i] = v;
} else {
log.duplicates += 1;
}
}
(PhaseSeries { t0, tau0, x }, log)
}
pub fn len(&self) -> usize {
self.x.len()
}
pub fn is_empty(&self) -> bool {
self.x.is_empty()
}
pub fn valid(&self) -> usize {
self.x.iter().filter(|v| v.is_finite()).count()
}
pub fn slice(&self, a: usize, b: usize) -> PhaseSeries {
let b = b.min(self.x.len());
let a = a.min(b);
PhaseSeries {
t0: self.t0 + a as f64 * self.tau0,
tau0: self.tau0,
x: self.x[a..b].to_vec(),
}
}
pub fn split_thirds(&self) -> (PhaseSeries, PhaseSeries) {
let k = self.x.len() / 3;
(self.slice(0, k), self.slice(k, self.x.len()))
}
pub fn longest_run(&self) -> PhaseSeries {
let (mut best_a, mut best_len) = (0usize, 0usize);
let mut a = 0usize;
for i in 0..=self.x.len() {
let present = i < self.x.len() && self.x[i].is_finite();
if !present {
if i - a > best_len {
best_a = a;
best_len = i - a;
}
a = i + 1;
}
}
self.slice(best_a, best_a + best_len)
}
}
pub fn gappy_oavar(x: &[f64], tau0: f64, m: usize) -> Option<(f64, usize)> {
if m == 0 || x.len() <= 2 * m {
return None;
}
let tau = m as f64 * tau0;
let (mut s, mut c) = (0.0, 0usize);
for i in 0..x.len() - 2 * m {
let d = x[i + 2 * m] - 2.0 * x[i + m] + x[i];
if d.is_finite() {
s += d * d;
c += 1;
}
}
(c > 0).then(|| (s / (2.0 * c as f64 * tau * tau), c))
}
pub fn gappy_ohvar(x: &[f64], tau0: f64, m: usize) -> Option<(f64, usize)> {
if m == 0 || x.len() <= 3 * m {
return None;
}
let tau = m as f64 * tau0;
let (mut s, mut c) = (0.0, 0usize);
for i in 0..x.len() - 3 * m {
let d = x[i + 3 * m] - 3.0 * x[i + 2 * m] + 3.0 * x[i + m] - x[i];
if d.is_finite() {
s += d * d;
c += 1;
}
}
(c > 0).then(|| (s / (6.0 * c as f64 * tau * tau), c))
}
pub fn remove_quadratic(s: &PhaseSeries) -> (PhaseSeries, [f64; 3]) {
let pts: Vec<(f64, f64)> =
s.x.iter()
.enumerate()
.filter(|(_, v)| v.is_finite())
.map(|(i, v)| (i as f64 * s.tau0, *v))
.collect();
if pts.len() < 3 {
return (s.clone(), [0.0; 3]);
}
let tc = pts.iter().map(|p| p.0).sum::<f64>() / pts.len() as f64;
let sc = pts
.iter()
.map(|p| (p.0 - tc).abs())
.fold(0.0_f64, f64::max)
.max(1.0);
let mut a = [[0.0f64; 3]; 3];
let mut b = [0.0f64; 3];
for &(t, v) in &pts {
let u = (t - tc) / sc;
let p = [1.0, u, u * u];
for r in 0..3 {
b[r] += p[r] * v;
for c in 0..3 {
a[r][c] += p[r] * p[c];
}
}
}
let Some(k) = solve3(a, b) else {
return (s.clone(), [0.0; 3]);
};
let c2 = k[2] / (sc * sc);
let c1 = k[1] / sc - 2.0 * k[2] * tc / (sc * sc);
let c0 = k[0] - k[1] * tc / sc + k[2] * tc * tc / (sc * sc);
let x =
s.x.iter()
.enumerate()
.map(|(i, v)| {
let u = (i as f64 * s.tau0 - tc) / sc;
v - (k[0] + k[1] * u + k[2] * u * u)
})
.collect();
(
PhaseSeries {
t0: s.t0,
tau0: s.tau0,
x,
},
[c0, c1, c2],
)
}
#[allow(clippy::needless_range_loop)]
fn solve3(mut a: [[f64; 3]; 3], mut b: [f64; 3]) -> Option<[f64; 3]> {
for col in 0..3 {
let piv = (col..3).max_by(|&i, &j| a[i][col].abs().total_cmp(&a[j][col].abs()))?;
if a[piv][col].abs() < 1e-300 {
return None;
}
a.swap(col, piv);
b.swap(col, piv);
for row in col + 1..3 {
let f = a[row][col] / a[col][col];
for c in col..3 {
a[row][c] -= f * a[col][c];
}
b[row] -= f * b[col];
}
}
let mut x = [0.0; 3];
for i in (0..3).rev() {
let mut acc = b[i];
for j in i + 1..3 {
acc -= a[i][j] * x[j];
}
x[i] = acc / a[i][i];
}
Some(x)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::portable_math::PortableFloat;
#[test]
fn gridding_keeps_gaps_and_counts_rejects() {
let s = [
(0.0, 1.0),
(30.0, 2.0),
(30.0, 9.0),
(90.0, 4.0),
(100.0, 5.0),
(120.0, f64::NAN),
];
let (p, log) = PhaseSeries::from_samples(&s, 30.0);
assert_eq!(p.len(), 4);
assert_eq!(p.x[1], 2.0);
assert!(p.x[2].is_nan());
assert_eq!(
log,
GriddingLog {
off_grid: 1,
duplicates: 1,
non_finite: 1
}
);
assert_eq!(p.valid(), 3);
}
#[test]
fn gap_free_variances_match_the_allan_module() {
let x: Vec<f64> = (0..200)
.map(|i| ((i * 7919) % 101) as f64 * 1e-9 + i as f64 * 1e-10)
.collect();
for m in [1, 2, 4, 8] {
let (v, c) = gappy_oavar(&x, 1.0, m).unwrap();
let a = crate::allan::overlapping_adev(&x, 1.0, m);
assert!((v.sqrt() - a).abs() / a < 1e-12);
assert_eq!(c, 200 - 2 * m);
let (h, _) = gappy_ohvar(&x, 1.0, m).unwrap();
let ha = crate::allan::hadamard_adev(&x, 1.0, m);
assert!((h.sqrt() - ha).abs() / ha < 1e-12);
}
}
#[test]
fn a_gap_removes_exactly_the_terms_that_touch_it() {
let mut x: Vec<f64> = (0..50).map(|i| (i as f64).psin()).collect();
x[20] = f64::NAN;
let (_, c1) = gappy_oavar(&x, 1.0, 1).unwrap();
assert_eq!(c1, 48 - 3);
let (_, c4) = gappy_oavar(&x, 1.0, 4).unwrap();
assert_eq!(c4, 42 - 3);
}
#[test]
fn quadratic_removal_recovers_the_drift() {
let s = PhaseSeries {
t0: 0.0,
tau0: 10.0,
x: (0..500)
.map(|i| {
let t = i as f64 * 10.0;
1e-6 + 2e-9 * t + 0.5 * 3e-14 * t * t
})
.collect(),
};
let (r, c) = remove_quadratic(&s);
assert!((2.0 * c[2] - 3e-14).abs() / 3e-14 < 1e-6, "{c:?}");
assert!((c[1] - 2e-9).abs() / 2e-9 < 1e-6);
assert!(r.x.iter().all(|v| v.abs() < 1e-15));
}
#[test]
fn thirds_and_longest_run() {
let mut s = PhaseSeries {
t0: 100.0,
tau0: 1.0,
x: (0..10).map(|i| i as f64).collect(),
};
let (a, b) = s.split_thirds();
assert_eq!((a.len(), b.len()), (3, 7));
assert_eq!(b.t0, 103.0);
s.x[4] = f64::NAN;
let r = s.longest_run();
assert_eq!(r.x, vec![5.0, 6.0, 7.0, 8.0, 9.0]);
}
}