use crate::error::{FFTError, FFTResult};
use crate::fft::fft;
use std::f64::consts::PI;
fn autocovariance(x: &[f64], max_lag: usize) -> Vec<f64> {
let n = x.len() as f64;
(0..=max_lag)
.map(|lag| {
let sum: f64 = x[..x.len() - lag]
.iter()
.zip(x[lag..].iter())
.map(|(&a, &b)| a * b)
.sum();
sum / n
})
.collect()
}
fn levinson_durbin(r: &[f64], b: &[f64]) -> FFTResult<Vec<f64>> {
let p = b.len();
if p == 0 {
return Ok(vec![]);
}
if r.len() < p + 1 {
return Err(FFTError::InvalidInput(
"levinson_durbin: r must have at least p+1 elements".into(),
));
}
let mut f = vec![0.0f64; p]; let mut g = vec![0.0f64; p]; f[0] = -r[1] / r[0];
g[0] = -r[1] / r[0];
let mut alpha = r[0];
let mut ar = vec![0.0f64; p];
ar[0] = f[0];
for i in 1..p {
alpha = alpha * (1.0 - f[i - 1].powi(2));
if alpha.abs() < f64::EPSILON * 1e6 {
return Err(FFTError::ComputationError(
"levinson_durbin: singular Toeplitz matrix (zero prediction error)".into(),
));
}
let mut num = r[i + 1];
for j in 0..i {
num += ar[j] * r[i - j];
}
let ki = -num / alpha;
let old_ar = ar[..i].to_vec();
ar[i] = ki;
for j in 0..i {
ar[j] += ki * old_ar[i - 1 - j];
}
let _beta = r[i + 1] + ar[..i].iter().zip(r[1..=i].iter()).map(|(&a, &ri)| a * ri).sum::<f64>();
}
Ok(ar)
}
fn ar_to_spectrum(ar_coeffs: &[f64], sigma2: f64, n_fft: usize) -> Vec<f64> {
let p = ar_coeffs.len();
(0..n_fft)
.map(|k| {
let omega = 2.0 * PI * k as f64 / n_fft as f64;
let mut re = 1.0_f64;
let mut im = 0.0_f64;
for (m, &a) in ar_coeffs.iter().enumerate() {
let angle = omega * (m + 1) as f64;
re += a * angle.cos();
im -= a * angle.sin();
}
sigma2 / (re * re + im * im)
})
.collect()
}
fn covariance_matrix(signal: &[f64], model_order: usize) -> Vec<f64> {
let r = autocovariance(signal, model_order);
let m = model_order;
let mut cov = vec![0.0f64; m * m];
for i in 0..m {
for j in 0..m {
let lag = if i >= j { i - j } else { j - i };
cov[i * m + j] = r[lag];
}
}
cov
}
fn eigen_symmetric(matrix: &[f64], n: usize) -> FFTResult<(Vec<f64>, Vec<Vec<f64>>)> {
if matrix.len() != n * n {
return Err(FFTError::InvalidInput(
"eigen_symmetric: matrix size mismatch".into(),
));
}
const MAX_ITER: usize = 200;
const TOL: f64 = 1e-12;
let mut a = matrix.to_vec();
let mut v: Vec<f64> = (0..n * n)
.map(|idx| if idx / n == idx % n { 1.0 } else { 0.0 })
.collect();
for _ in 0..MAX_ITER {
let mut max_val = 0.0f64;
let mut p = 0;
let mut q = 1;
for i in 0..n {
for j in (i + 1)..n {
let val = a[i * n + j].abs();
if val > max_val {
max_val = val;
p = i;
q = j;
}
}
}
if max_val < TOL {
break;
}
let theta = if (a[q * n + q] - a[p * n + p]).abs() < TOL {
PI / 4.0
} else {
0.5 * ((2.0 * a[p * n + q]) / (a[q * n + q] - a[p * n + p])).atan()
};
let c = theta.cos();
let s = theta.sin();
let mut a_new = a.clone();
for i in 0..n {
let api = a[p * n + i];
let aqi = a[q * n + i];
a_new[p * n + i] = c * api - s * aqi;
a_new[q * n + i] = s * api + c * aqi;
}
let mut a2 = a_new.clone();
for i in 0..n {
let aip = a_new[i * n + p];
let aiq = a_new[i * n + q];
a2[i * n + p] = c * aip - s * aiq;
a2[i * n + q] = s * aip + c * aiq;
}
for i in 0..n {
for j in 0..n {
a2[j * n + i] = a2[i * n + j];
}
}
a = a2;
for i in 0..n {
let vip = v[i * n + p];
let viq = v[i * n + q];
v[i * n + p] = c * vip - s * viq;
v[i * n + q] = s * vip + c * viq;
}
}
let eigenvalues: Vec<f64> = (0..n).map(|i| a[i * n + i]).collect();
let eigenvectors: Vec<Vec<f64>> = (0..n).map(|j| (0..n).map(|i| v[i * n + j]).collect()).collect();
let mut pairs: Vec<(f64, Vec<f64>)> = eigenvalues
.into_iter()
.zip(eigenvectors.into_iter())
.collect();
pairs.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
let sorted_eigenvalues: Vec<f64> = pairs.iter().map(|(e, _)| *e).collect();
let sorted_eigenvectors: Vec<Vec<f64>> = pairs.into_iter().map(|(_, v)| v).collect();
Ok((sorted_eigenvalues, sorted_eigenvectors))
}
pub fn burg_ar(signal: &[f64], order: usize) -> FFTResult<(Vec<f64>, f64)> {
let n = signal.len();
if n < order + 2 {
return Err(FFTError::InvalidInput(format!(
"burg_ar: signal length {} must be > order+1 = {}",
n,
order + 1
)));
}
let mut f = signal.to_vec(); let mut b = signal.to_vec(); let mut a = vec![0.0f64; order]; let mut sigma2 = signal.iter().map(|&x| x * x).sum::<f64>() / n as f64;
for m in 0..order {
let num: f64 = -2.0
* f[m + 1..]
.iter()
.zip(b[..n - m - 1].iter())
.map(|(&fi, &bi)| fi * bi)
.sum::<f64>();
let den: f64 = f[m + 1..].iter().map(|&fi| fi * fi).sum::<f64>()
+ b[..n - m - 1].iter().map(|&bi| bi * bi).sum::<f64>();
if den.abs() < f64::EPSILON * 1e6 {
break; }
let km = num / den;
let old_a = a[..m].to_vec();
a[m] = km;
for j in 0..m {
a[j] += km * old_a[m - 1 - j];
}
let f_prev = f.clone();
let b_prev = b.clone();
for i in m + 1..n {
f[i] = f_prev[i] + km * b_prev[i - 1];
b[i] = b_prev[i - 1] + km * f_prev[i];
}
sigma2 *= 1.0 - km * km;
}
Ok((a, sigma2))
}
pub fn burg_spectrum(
signal: &[f64],
order: usize,
n_fft: usize,
sample_rate: f64,
) -> FFTResult<Vec<f64>> {
let (ar, sigma2) = burg_ar(signal, order)?;
if n_fft < 2 {
return Err(FFTError::InvalidInput(
"burg_spectrum: n_fft must be >= 2".into(),
));
}
let n_out = n_fft / 2 + 1;
let full = ar_to_spectrum(&ar, sigma2, n_fft);
let scale = 1.0 / sample_rate;
Ok(full[..n_out].iter().map(|&v| v * scale).collect())
}
pub fn yule_walker_ar(signal: &[f64], order: usize) -> FFTResult<(Vec<f64>, f64)> {
let n = signal.len();
if n < order + 2 {
return Err(FFTError::InvalidInput(format!(
"yule_walker_ar: signal length {} too short for order {}",
n, order
)));
}
let r = autocovariance(signal, order);
if r[0].abs() < f64::EPSILON {
return Err(FFTError::ComputationError(
"yule_walker_ar: zero-variance signal".into(),
));
}
let mut a = vec![0.0f64; order]; a[0] = -r[1] / r[0];
let mut alpha = r[0] * (1.0 - a[0].powi(2));
for i in 1..order {
if alpha.abs() < f64::EPSILON * r[0].abs() {
return Err(FFTError::ComputationError(
"yule_walker_ar: singular Toeplitz matrix".into(),
));
}
let num: f64 = r[i + 1] + (0..i).map(|j| a[j] * r[i - j]).sum::<f64>();
let ki = -num / alpha;
let old = a[..i].to_vec();
a[i] = ki;
for j in 0..i {
a[j] += ki * old[i - 1 - j];
}
alpha *= 1.0 - ki * ki;
}
let sigma2 = r[0]
+ (0..order)
.map(|j| a[j] * r[j + 1])
.sum::<f64>();
let sigma2 = sigma2.max(0.0);
Ok((a, sigma2))
}
pub fn yule_walker_spectrum(
signal: &[f64],
order: usize,
n_fft: usize,
sample_rate: f64,
) -> FFTResult<Vec<f64>> {
let (ar, sigma2) = yule_walker_ar(signal, order)?;
if n_fft < 2 {
return Err(FFTError::InvalidInput(
"yule_walker_spectrum: n_fft must be >= 2".into(),
));
}
let n_out = n_fft / 2 + 1;
let full = ar_to_spectrum(&ar, sigma2, n_fft);
let scale = 1.0 / sample_rate;
Ok(full[..n_out].iter().map(|&v| v * scale).collect())
}
pub fn music_spectrum(
signal: &[f64],
n_sources: usize,
n_fft: usize,
model_order: usize,
) -> FFTResult<Vec<f64>> {
let m = if model_order == 0 {
2 * n_sources + 2
} else {
model_order
};
if n_sources >= m {
return Err(FFTError::InvalidInput(
"music_spectrum: n_sources must be < model_order".into(),
));
}
if signal.len() < m + 1 {
return Err(FFTError::InvalidInput(format!(
"music_spectrum: signal length {} too short for model_order {}",
signal.len(),
m
)));
}
if n_fft < 2 {
return Err(FFTError::InvalidInput(
"music_spectrum: n_fft must be >= 2".into(),
));
}
let cov = covariance_matrix(signal, m);
let (_eigenvalues, eigenvectors) = eigen_symmetric(&cov, m)?;
let n_noise = m - n_sources;
let noise_vecs: Vec<&Vec<f64>> = eigenvectors[..n_noise].iter().collect();
let n_out = n_fft / 2 + 1;
let mut ps = vec![0.0f64; n_out];
for k in 0..n_out {
let omega = 2.0 * PI * k as f64 / n_fft as f64;
let mut denom = 0.0_f64;
for nv in &noise_vecs {
let mut re = 0.0_f64;
let mut im = 0.0_f64;
for (i, &v_i) in nv.iter().enumerate() {
re += (omega * i as f64).cos() * v_i;
im -= (omega * i as f64).sin() * v_i;
}
denom += re * re + im * im;
}
ps[k] = if denom.abs() < f64::EPSILON { f64::MAX } else { 1.0 / denom };
}
Ok(ps)
}
pub fn music_frequencies(
signal: &[f64],
n_sources: usize,
n_fft: usize,
model_order: usize,
sample_rate: f64,
) -> FFTResult<Vec<f64>> {
let ps = music_spectrum(signal, n_sources, n_fft, model_order)?;
let n_out = ps.len();
let mut candidates: Vec<(usize, f64)> = (1..n_out - 1)
.filter(|&i| ps[i] >= ps[i - 1] && ps[i] >= ps[i + 1])
.map(|i| (i, ps[i]))
.collect();
candidates.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
let mut freqs: Vec<f64> = candidates
.iter()
.take(n_sources)
.map(|(idx, _)| {
*idx as f64 * sample_rate / n_fft as f64
})
.collect();
freqs.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
Ok(freqs)
}
pub fn pisarenko(signal: &[f64], n_sources: usize) -> FFTResult<Vec<f64>> {
let m = n_sources + 1; if signal.len() < 2 * m {
return Err(FFTError::InvalidInput(format!(
"pisarenko: signal length {} too short (need >= {})",
signal.len(),
2 * m
)));
}
let cov = covariance_matrix(signal, m);
let (_eigenvalues, eigenvectors) = eigen_symmetric(&cov, m)?;
let min_vec = &eigenvectors[0];
let freqs = polynomial_unit_circle_roots(min_vec)?;
Ok(freqs)
}
fn polynomial_unit_circle_roots(coeffs: &[f64]) -> FFTResult<Vec<f64>> {
let p = coeffs.len() - 1; if p == 0 {
return Ok(vec![]);
}
if p == 1 {
if coeffs[1].abs() < f64::EPSILON {
return Err(FFTError::ComputationError(
"polynomial_unit_circle_roots: leading coefficient is zero".into(),
));
}
let z_re = -coeffs[0] / coeffs[1];
if (z_re.abs() - 1.0).abs() < 0.1 {
let freq = z_re.atan2(0.0_f64).abs() / (2.0 * PI);
return Ok(vec![freq]);
}
return Ok(vec![]);
}
let lead = coeffs[p];
if lead.abs() < f64::EPSILON {
return Err(FFTError::ComputationError(
"polynomial_unit_circle_roots: leading coefficient near zero".into(),
));
}
let n_grid = 4096;
let mut roots = Vec::new();
let mut prev_re = 0.0f64;
let mut prev_mag = f64::MAX;
for k in 0..=n_grid {
let omega = PI * k as f64 / n_grid as f64;
let z_re = omega.cos();
let z_im = omega.sin();
let mut re = coeffs[p];
let mut im = 0.0f64;
for j in (0..p).rev() {
let new_re = re * z_re - im * z_im + coeffs[j];
let new_im = re * z_im + im * z_re;
re = new_re;
im = new_im;
}
let mag = (re * re + im * im).sqrt();
if k > 0 && mag < prev_mag && prev_mag < 0.1 * lead.abs() * (p as f64) {
let freq_cycles = (omega - PI / n_grid as f64) / (2.0 * PI);
if freq_cycles >= 0.0 && freq_cycles <= 0.5 {
roots.push(freq_cycles);
}
}
if k > 0 && prev_re * re < 0.0 && im.abs() < (lead.abs() * (p as f64)).max(1e-6) {
let freq_cycles = (omega - PI / (2.0 * n_grid as f64)) / (2.0 * PI);
if freq_cycles >= 0.0 && freq_cycles <= 0.5 {
roots.push(freq_cycles);
}
}
prev_re = re;
prev_mag = mag;
}
roots.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let mut unique: Vec<f64> = Vec::new();
for f in roots {
if unique.last().map(|&last| (f - last).abs() > 0.005).unwrap_or(true) {
unique.push(f);
}
}
Ok(unique)
}
pub fn capon_spectrum(
signal: &[f64],
n_fft: usize,
model_order: usize,
) -> FFTResult<Vec<f64>> {
let m = if model_order == 0 { 16 } else { model_order };
if signal.len() < m + 1 {
return Err(FFTError::InvalidInput(format!(
"capon_spectrum: signal too short ({} < {})",
signal.len(),
m + 1
)));
}
if n_fft < 2 {
return Err(FFTError::InvalidInput(
"capon_spectrum: n_fft must be >= 2".into(),
));
}
let cov = covariance_matrix(signal, m);
let inv = invert_symmetric_pos_def(&cov, m)?;
let n_out = n_fft / 2 + 1;
let mut ps = vec![0.0f64; n_out];
for k in 0..n_out {
let omega = 2.0 * PI * k as f64 / n_fft as f64;
let mut quad = 0.0f64;
for i in 0..m {
for j in 0..m {
let diff = i as f64 - j as f64;
quad += inv[i * m + j] * (omega * diff).cos();
}
}
ps[k] = if quad.abs() < f64::EPSILON { f64::MAX } else { 1.0 / quad };
}
Ok(ps)
}
fn invert_symmetric_pos_def(a: &[f64], n: usize) -> FFTResult<Vec<f64>> {
let reg = {
let diag_max = (0..n).map(|i| a[i * n + i]).fold(0.0f64, f64::max);
diag_max * 1e-10
};
let mut l = vec![0.0f64; n * n];
for i in 0..n {
for j in 0..=i {
let mut s = a[i * n + j];
s += if i == j { reg } else { 0.0 };
for k in 0..j {
s -= l[i * n + k] * l[j * n + k];
}
l[i * n + j] = if i == j {
if s < 0.0 {
return Err(FFTError::ComputationError(
"invert_symmetric_pos_def: matrix not positive definite".into(),
));
}
s.sqrt()
} else {
if l[j * n + j].abs() < f64::EPSILON {
0.0
} else {
s / l[j * n + j]
}
};
}
}
let mut y = vec![0.0f64; n * n];
for col in 0..n {
for i in 0..n {
let mut s = if i == col { 1.0 } else { 0.0 };
for k in 0..i {
s -= l[i * n + k] * y[k * n + col];
}
y[i * n + col] = if l[i * n + i].abs() < f64::EPSILON {
0.0
} else {
s / l[i * n + i]
};
}
}
let mut x = vec![0.0f64; n * n];
for col in 0..n {
for i in (0..n).rev() {
let mut s = y[i * n + col];
for k in (i + 1)..n {
s -= l[k * n + i] * x[k * n + col];
}
x[i * n + col] = if l[i * n + i].abs() < f64::EPSILON {
0.0
} else {
s / l[i * n + i]
};
}
}
Ok(x)
}
pub fn esprit_frequencies(
signal: &[f64],
n_sources: usize,
model_order: usize,
sample_rate: f64,
) -> FFTResult<Vec<f64>> {
let m = if model_order == 0 {
(2 * n_sources + 2).max(4)
} else {
model_order
};
if n_sources >= m {
return Err(FFTError::InvalidInput(
"esprit_frequencies: n_sources must be < model_order".into(),
));
}
if signal.len() < m + 1 {
return Err(FFTError::InvalidInput(format!(
"esprit_frequencies: signal length {} too short for model_order {}",
signal.len(),
m
)));
}
let cov = covariance_matrix(signal, m);
let (_eigenvalues, eigenvectors) = eigen_symmetric(&cov, m)?;
let n = m;
let signal_vecs: Vec<&Vec<f64>> = eigenvectors[n - n_sources..].iter().collect();
let e_rows_e1: usize = n - 1;
let e_rows_e2: usize = n - 1;
let mut e1 = vec![0.0f64; e_rows_e1 * n_sources];
let mut e2 = vec![0.0f64; e_rows_e2 * n_sources];
for (col, sv) in signal_vecs.iter().enumerate() {
for row in 0..e_rows_e1 {
e1[row * n_sources + col] = sv[row];
e2[row * n_sources + col] = sv[row + 1];
}
}
let mut gtg = vec![0.0f64; n_sources * n_sources];
for i in 0..n_sources {
for j in 0..n_sources {
for r in 0..e_rows_e1 {
gtg[i * n_sources + j] += e1[r * n_sources + i] * e1[r * n_sources + j];
}
}
}
let mut gty = vec![0.0f64; n_sources * n_sources];
for i in 0..n_sources {
for j in 0..n_sources {
for r in 0..e_rows_e1 {
gty[i * n_sources + j] += e1[r * n_sources + i] * e2[r * n_sources + j];
}
}
}
let inv_gtg = invert_symmetric_pos_def(>g, n_sources)?;
let mut phi = vec![0.0f64; n_sources * n_sources];
for i in 0..n_sources {
for j in 0..n_sources {
for k in 0..n_sources {
phi[i * n_sources + j] += inv_gtg[i * n_sources + k] * gty[k * n_sources + j];
}
}
}
let mut s = vec![0.0f64; n_sources * n_sources];
for i in 0..n_sources {
for j in 0..n_sources {
s[i * n_sources + j] = (phi[i * n_sources + j] - phi[j * n_sources + i]) * 0.5;
}
}
let (s_evals, _) = eigen_symmetric(&s, n_sources)?;
let mut freqs: Vec<f64> = s_evals
.iter()
.map(|&omega| {
let omega_clamped = omega.max(-1.0).min(1.0);
let freq_norm = omega_clamped.asin() / PI; (freq_norm.abs() * sample_rate).max(0.0)
})
.collect();
freqs.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
Ok(freqs)
}
#[derive(Debug, Clone)]
pub struct LombScargleResult {
pub frequencies: Vec<f64>,
pub power: Vec<f64>,
pub false_alarm_prob: Vec<f64>,
}
pub fn lomb_scargle(
times: &[f64],
values: &[f64],
frequencies: &[f64],
normalize: bool,
) -> FFTResult<LombScargleResult> {
let n = times.len();
if n == 0 {
return Err(FFTError::InvalidInput("lomb_scargle: empty input".into()));
}
if values.len() != n {
return Err(FFTError::InvalidInput(
"lomb_scargle: times and values must have same length".into(),
));
}
if frequencies.is_empty() {
return Err(FFTError::InvalidInput(
"lomb_scargle: frequencies must not be empty".into(),
));
}
let mean = values.iter().sum::<f64>() / n as f64;
let y: Vec<f64> = values.iter().map(|&v| v - mean).collect();
let variance: f64 = y.iter().map(|&v| v * v).sum::<f64>() / n as f64;
let nf = frequencies.len();
let mut power = vec![0.0f64; nf];
for (fi, &freq) in frequencies.iter().enumerate() {
let omega = 2.0 * PI * freq;
let sum_sin2: f64 = times.iter().map(|&t| (2.0 * omega * t).sin()).sum();
let sum_cos2: f64 = times.iter().map(|&t| (2.0 * omega * t).cos()).sum();
let tau = sum_sin2.atan2(sum_cos2) / (2.0 * omega);
let sum_ycos: f64 = times
.iter()
.zip(y.iter())
.map(|(&t, &yi)| yi * (omega * (t - tau)).cos())
.sum();
let sum_ysin: f64 = times
.iter()
.zip(y.iter())
.map(|(&t, &yi)| yi * (omega * (t - tau)).sin())
.sum();
let sum_cos2_tau: f64 = times
.iter()
.map(|&t| (omega * (t - tau)).cos().powi(2))
.sum();
let sum_sin2_tau: f64 = times
.iter()
.map(|&t| (omega * (t - tau)).sin().powi(2))
.sum();
let p = if sum_cos2_tau.abs() < f64::EPSILON || sum_sin2_tau.abs() < f64::EPSILON {
0.0
} else {
0.5 * (sum_ycos * sum_ycos / sum_cos2_tau + sum_ysin * sum_ysin / sum_sin2_tau)
};
power[fi] = if normalize && variance > f64::EPSILON {
p / variance
} else {
p
};
}
let false_alarm_prob: Vec<f64> = power
.iter()
.map(|&p| {
let z = if normalize { p } else { p / variance.max(f64::EPSILON) };
(-z).exp().min(1.0)
})
.collect();
Ok(LombScargleResult {
frequencies: frequencies.to_vec(),
power,
false_alarm_prob,
})
}
#[derive(Debug, Clone)]
pub struct MultitaperResult {
pub frequencies: Vec<f64>,
pub psd: Vec<f64>,
pub confidence_5pct: Vec<f64>,
pub confidence_95pct: Vec<f64>,
pub adaptive_weights: Vec<Vec<f64>>,
}
pub fn multitaper_psd(
signal: &[f64],
sample_rate: f64,
n_tapers: usize,
time_half_bandwidth: f64,
) -> FFTResult<MultitaperResult> {
let n = signal.len();
if n == 0 {
return Err(FFTError::InvalidInput("multitaper_psd: empty signal".into()));
}
if n_tapers == 0 {
return Err(FFTError::InvalidInput(
"multitaper_psd: n_tapers must be >= 1".into(),
));
}
let tapers = crate::window_functions::dpss(n, time_half_bandwidth, n_tapers)?;
let n_fft = n;
let n_out = n_fft / 2 + 1;
let mut eigenspectra = vec![vec![0.0f64; n_out]; n_tapers];
for (k, taper) in tapers.iter().enumerate() {
let windowed: Vec<f64> = signal.iter().zip(taper.iter()).map(|(&x, &h)| x * h).collect();
let spectrum = fft(&windowed, Some(n_fft))?;
for f in 0..n_out {
eigenspectra[k][f] = spectrum[f].norm_sqr() / sample_rate;
}
}
let mut psd = vec![0.0f64; n_out];
for f in 0..n_out {
psd[f] = eigenspectra.iter().map(|s| s[f]).sum::<f64>() / n_tapers as f64;
}
let sigma2 = psd.iter().sum::<f64>() / n_out as f64;
let mut weights = vec![vec![0.0f64; n_tapers]; n_out];
for _iter in 0..3 {
for f in 0..n_out {
let mut wsum = 0.0f64;
let mut psd_new = 0.0f64;
for k in 0..n_tapers {
let bk = psd[f] / (psd[f] + sigma2);
let wk = bk * bk;
weights[f][k] = wk;
psd_new += wk * eigenspectra[k][f];
wsum += wk;
}
psd[f] = if wsum > f64::EPSILON { psd_new / wsum } else { 0.0 };
}
}
let dof = 2.0 * n_tapers as f64;
let chi2_05 = dof * (1.0 - 2.0 / (9.0 * dof) + 1.645 * (2.0 / (9.0 * dof)).sqrt()).powi(3);
let chi2_95 = dof * (1.0 - 2.0 / (9.0 * dof) - 1.645 * (2.0 / (9.0 * dof)).sqrt()).powi(3);
let confidence_5pct: Vec<f64> = psd.iter().map(|&p| dof * p / chi2_05.max(f64::EPSILON)).collect();
let confidence_95pct: Vec<f64> = psd.iter().map(|&p| dof * p / chi2_95.max(f64::EPSILON)).collect();
let frequencies: Vec<f64> = (0..n_out)
.map(|k| k as f64 * sample_rate / n_fft as f64)
.collect();
Ok(MultitaperResult {
frequencies,
psd,
confidence_5pct,
confidence_95pct,
adaptive_weights: weights,
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::f64::consts::PI;
fn sinusoid(n: usize, freq_norm: f64, phase: f64) -> Vec<f64> {
(0..n)
.map(|i| (2.0 * PI * freq_norm * i as f64 + phase).cos())
.collect()
}
#[test]
fn test_burg_ar_length() {
let signal = sinusoid(256, 0.1, 0.0);
let (ar, sigma2) = burg_ar(&signal, 4).expect("burg_ar");
assert_eq!(ar.len(), 4);
assert!(sigma2 >= 0.0, "sigma2 should be non-negative");
}
#[test]
fn test_burg_spectrum_length() {
let fs = 1000.0_f64;
let signal: Vec<f64> = (0..512)
.map(|i| (2.0 * PI * 100.0 * i as f64 / fs).sin())
.collect();
let psd = burg_spectrum(&signal, 8, 512, fs).expect("burg_spectrum");
assert_eq!(psd.len(), 512 / 2 + 1);
for &v in &psd {
assert!(v >= 0.0, "PSD values must be non-negative");
}
}
#[test]
fn test_yule_walker_ar_length() {
let signal = sinusoid(256, 0.15, 0.3);
let (ar, sigma2) = yule_walker_ar(&signal, 6).expect("yule_walker_ar");
assert_eq!(ar.len(), 6);
assert!(sigma2 >= 0.0, "sigma2 must be non-negative");
}
#[test]
fn test_yule_walker_spectrum_length() {
let fs = 500.0_f64;
let signal: Vec<f64> = (0..512)
.map(|i| (2.0 * PI * 50.0 * i as f64 / fs).sin())
.collect();
let psd = yule_walker_spectrum(&signal, 4, 256, fs).expect("yule_walker_spectrum");
assert_eq!(psd.len(), 256 / 2 + 1);
}
#[test]
fn test_music_spectrum_length() {
let signal = sinusoid(256, 0.1, 0.0);
let ps = music_spectrum(&signal, 1, 256, 0).expect("music_spectrum");
assert_eq!(ps.len(), 256 / 2 + 1);
}
#[test]
fn test_music_frequencies_count() {
let fs = 1000.0_f64;
let n = 512usize;
let signal: Vec<f64> = (0..n)
.map(|i| {
(2.0 * PI * 100.0 * i as f64 / fs).sin()
+ (2.0 * PI * 300.0 * i as f64 / fs).sin()
})
.collect();
let freqs = music_frequencies(&signal, 2, 2048, 0, fs).expect("music_frequencies");
assert_eq!(freqs.len(), 2);
for &f in &freqs {
assert!(f >= 0.0);
}
}
#[test]
fn test_pisarenko_single_source() {
let n = 256usize;
let f0 = 0.1_f64;
let signal = sinusoid(n, f0, 0.0);
match pisarenko(&signal, 1) {
Ok(freqs) => {
assert!(!freqs.is_empty(), "should find at least one frequency");
}
Err(e) => {
eprintln!("pisarenko returned error (acceptable for pure tone): {e}");
}
}
}
#[test]
fn test_capon_spectrum_length() {
let signal = sinusoid(256, 0.2, 0.5);
let ps = capon_spectrum(&signal, 256, 16).expect("capon_spectrum");
assert_eq!(ps.len(), 256 / 2 + 1);
for &v in &ps {
assert!(v >= 0.0, "Capon pseudo-spectrum must be non-negative");
}
}
#[test]
fn test_lomb_scargle_output_length() {
let times: Vec<f64> = (0..100).map(|i| i as f64 * 0.1).collect();
let values: Vec<f64> = times.iter().map(|&t| (2.0 * PI * 2.0 * t).sin()).collect();
let freqs: Vec<f64> = (1..20).map(|k| k as f64 * 0.5).collect();
let result = lomb_scargle(×, &values, &freqs, true).expect("lomb_scargle");
assert_eq!(result.frequencies.len(), freqs.len());
assert_eq!(result.power.len(), freqs.len());
assert_eq!(result.false_alarm_prob.len(), freqs.len());
for &p in &result.false_alarm_prob {
assert!(p >= 0.0 && p <= 1.0, "FAP must be in [0,1]");
}
}
#[test]
fn test_lomb_scargle_detects_frequency() {
let times: Vec<f64> = (0..200).map(|i| i as f64 * 0.05 + (i as f64).sin() * 0.01).collect();
let values: Vec<f64> = times.iter().map(|&t| (2.0 * PI * 3.0 * t).sin()).collect();
let freqs: Vec<f64> = (10..80).map(|k| k as f64 * 0.1).collect();
let result = lomb_scargle(×, &values, &freqs, true).expect("lomb_scargle");
let peak_idx = result
.power
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.map(|(idx, _)| idx)
.unwrap_or(0);
let peak_freq = result.frequencies[peak_idx];
assert!(
(peak_freq - 3.0).abs() < 0.5,
"Expected peak near 3 Hz, got {peak_freq:.2} Hz"
);
}
#[test]
fn test_multitaper_psd_output() {
let fs = 1000.0_f64;
let n = 1024usize;
let signal: Vec<f64> = (0..n)
.map(|i| (2.0 * PI * 100.0 * i as f64 / fs).sin())
.collect();
let result = multitaper_psd(&signal, fs, 4, 2.5).expect("multitaper_psd");
assert_eq!(result.frequencies.len(), n / 2 + 1);
assert_eq!(result.psd.len(), n / 2 + 1);
assert_eq!(result.confidence_5pct.len(), n / 2 + 1);
assert_eq!(result.confidence_95pct.len(), n / 2 + 1);
for &p in &result.psd {
assert!(p >= 0.0, "PSD must be non-negative");
}
}
#[test]
fn test_esprit_frequencies_count() {
let fs = 1000.0_f64;
let n = 256usize;
let signal: Vec<f64> = (0..n)
.map(|i| {
(2.0 * PI * 100.0 * i as f64 / fs).sin()
+ (2.0 * PI * 250.0 * i as f64 / fs).sin()
})
.collect();
let freqs = esprit_frequencies(&signal, 2, 0, fs).expect("esprit_frequencies");
assert_eq!(freqs.len(), 2);
for &f in &freqs {
assert!(f >= 0.0, "Frequencies must be non-negative");
assert!(f <= fs / 2.0, "Frequencies must not exceed Nyquist");
}
}
#[test]
fn test_burg_spectrum_peak_near_signal_freq() {
let fs = 1000.0_f64;
let n = 512usize;
let f_in = 200.0_f64; let signal: Vec<f64> = (0..n)
.map(|i| (2.0 * PI * f_in * i as f64 / fs).sin())
.collect();
let psd = burg_spectrum(&signal, 20, 512, fs).expect("burg_spectrum");
let peak_idx = psd
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.map(|(i, _)| i)
.unwrap_or(0);
let peak_freq = peak_idx as f64 * fs / 512.0;
assert!(
(peak_freq - f_in).abs() < 50.0,
"Expected peak near {f_in} Hz, got {peak_freq:.1} Hz"
);
}
}