use super::strength::seasonal_strength_variance;
use super::{
compute_mean_curve, find_peaks_1d, fit_and_subtract_sinusoid, periodogram, DetectedPeriod,
PeriodEstimate,
};
use crate::basis::fourier_basis_with_period;
use crate::matrix::FdMatrix;
use crate::slice_maybe_parallel;
#[cfg(feature = "parallel")]
use rayon::iter::ParallelIterator;
pub fn estimate_period_fft(data: &FdMatrix, argvals: &[f64]) -> PeriodEstimate {
let (n, m) = data.shape();
if n == 0 || m < 4 || argvals.len() != m {
return PeriodEstimate {
period: f64::NAN,
frequency: f64::NAN,
power: 0.0,
confidence: 0.0,
};
}
let mean_curve = compute_mean_curve(data);
let (frequencies, power) = periodogram(&mean_curve, argvals);
if frequencies.len() < 2 {
return PeriodEstimate {
period: f64::NAN,
frequency: f64::NAN,
power: 0.0,
confidence: 0.0,
};
}
let mut max_power = 0.0;
let mut max_idx = 1;
for (i, &p) in power.iter().enumerate().skip(1) {
if p > max_power {
max_power = p;
max_idx = i;
}
}
let dominant_freq = frequencies[max_idx];
let period = if dominant_freq > 1e-15 {
1.0 / dominant_freq
} else {
f64::INFINITY
};
let mean_power: f64 = power.iter().skip(1).sum::<f64>() / (power.len() - 1) as f64;
let confidence = if mean_power > 1e-15 {
max_power / mean_power
} else {
0.0
};
PeriodEstimate {
period,
frequency: dominant_freq,
power: max_power,
confidence,
}
}
pub fn estimate_period_acf(
data: &[f64],
n: usize,
m: usize,
argvals: &[f64],
max_lag: usize,
) -> PeriodEstimate {
if n == 0 || m < 4 || argvals.len() != m {
return PeriodEstimate {
period: f64::NAN,
frequency: f64::NAN,
power: 0.0,
confidence: 0.0,
};
}
let mat = FdMatrix::from_slice(data, n, m).expect("dimension invariant: data.len() == n * m");
let mean_curve = compute_mean_curve(&mat);
let acf = super::autocorrelation(&mean_curve, max_lag);
let min_lag = 2;
let peaks = find_peaks_1d(&acf[min_lag..], 1);
if peaks.is_empty() {
return PeriodEstimate {
period: f64::NAN,
frequency: f64::NAN,
power: 0.0,
confidence: 0.0,
};
}
let peak_lag = peaks[0] + min_lag;
let dt = (argvals[m - 1] - argvals[0]) / (m - 1) as f64;
let period = peak_lag as f64 * dt;
let frequency = if period > 1e-15 { 1.0 / period } else { 0.0 };
PeriodEstimate {
period,
frequency,
power: acf[peak_lag],
confidence: acf[peak_lag].abs(),
}
}
pub fn estimate_period_regression(
data: &[f64],
n: usize,
m: usize,
argvals: &[f64],
period_min: f64,
period_max: f64,
n_candidates: usize,
n_harmonics: usize,
) -> PeriodEstimate {
if n == 0 || m < 4 || argvals.len() != m || period_min >= period_max || n_candidates < 2 {
return PeriodEstimate {
period: f64::NAN,
frequency: f64::NAN,
power: 0.0,
confidence: 0.0,
};
}
let mat = FdMatrix::from_slice(data, n, m).expect("dimension invariant: data.len() == n * m");
let mean_curve = compute_mean_curve(&mat);
let nbasis = 1 + 2 * n_harmonics;
let candidates: Vec<f64> = (0..n_candidates)
.map(|i| period_min + (period_max - period_min) * i as f64 / (n_candidates - 1) as f64)
.collect();
let results: Vec<(f64, f64)> = slice_maybe_parallel!(candidates)
.map(|&period| {
let basis = fourier_basis_with_period(argvals, nbasis, period);
let mut rss = 0.0;
for j in 0..m {
let mut fitted = 0.0;
for k in 0..nbasis {
let b_val = basis[j + k * m];
let coef: f64 = (0..m)
.map(|l| mean_curve[l] * basis[l + k * m])
.sum::<f64>()
/ (0..m)
.map(|l| basis[l + k * m].powi(2))
.sum::<f64>()
.max(1e-15);
fitted += coef * b_val;
}
let resid = mean_curve[j] - fitted;
rss += resid * resid;
}
(period, rss)
})
.collect();
let (best_period, min_rss) = results
.iter()
.min_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
.copied()
.unwrap_or((f64::NAN, f64::INFINITY));
let mean_rss: f64 = results.iter().map(|(_, r)| r).sum::<f64>() / results.len() as f64;
let confidence = if min_rss > 1e-15 {
(mean_rss / min_rss).min(10.0)
} else {
10.0
};
PeriodEstimate {
period: best_period,
frequency: if best_period > 1e-15 {
1.0 / best_period
} else {
0.0
},
power: 1.0 - min_rss / mean_rss,
confidence,
}
}
pub fn detect_multiple_periods(
data: &[f64],
n: usize,
m: usize,
argvals: &[f64],
max_periods: usize,
min_confidence: f64,
min_strength: f64,
) -> Vec<DetectedPeriod> {
if n == 0 || m < 4 || argvals.len() != m || max_periods == 0 {
return Vec::new();
}
let mat = FdMatrix::from_slice(data, n, m).expect("dimension invariant: data.len() == n * m");
let mean_curve = compute_mean_curve(&mat);
let mut residual = mean_curve.clone();
let mut detected = Vec::with_capacity(max_periods);
for iteration in 1..=max_periods {
match evaluate_next_period(
&mut residual,
m,
argvals,
min_confidence,
min_strength,
iteration,
) {
Some(period) => detected.push(period),
None => break,
}
}
detected
}
fn evaluate_next_period(
residual: &mut [f64],
m: usize,
argvals: &[f64],
min_confidence: f64,
min_strength: f64,
iteration: usize,
) -> Option<DetectedPeriod> {
let residual_mat =
FdMatrix::from_slice(residual, 1, m).expect("dimension invariant: data.len() == n * m");
let est = estimate_period_fft(&residual_mat, argvals);
if est.confidence < min_confidence || est.period.is_nan() || est.period.is_infinite() {
return None;
}
let strength = seasonal_strength_variance(&residual_mat, argvals, est.period, 3);
if strength < min_strength || strength.is_nan() {
return None;
}
let (_a, _b, amplitude, phase) = fit_and_subtract_sinusoid(residual, argvals, est.period);
Some(DetectedPeriod {
period: est.period,
confidence: est.confidence,
strength,
amplitude,
phase,
iteration,
})
}