use std::f64::consts::PI;
use numr::algorithm::fft::{FftAlgorithms, FftNormalization};
use numr::dtype::Complex128;
use numr::runtime::cpu::{CpuClient, CpuDevice, CpuRuntime};
use numr::tensor::Tensor;
use super::pairwise::{pairwise_mean, pairwise_sum};
use crate::error::{Error, Result};
#[derive(Debug, Clone, PartialEq)]
pub struct Psd {
pub freqs: Vec<f64>,
pub power: Vec<f64>,
}
fn periodic_hann(n: usize) -> Vec<f64> {
if n <= 1 {
return vec![1.0; n];
}
let step = (PI - -PI) / n as f64;
(0..n)
.map(|i| 0.5 + 0.5 * (i as f64 * step + -PI).cos())
.collect()
}
pub fn welch(x: &[f64], fs: f64, nperseg: usize) -> Result<Psd> {
if x.is_empty() || nperseg == 0 || fs.partial_cmp(&0.0) != Some(std::cmp::Ordering::Greater) {
return Err(Error::InvalidArgument {
arg: "welch",
reason: format!(
"needs samples ({}), a segment length ({nperseg}) and a positive rate ({fs})",
x.len()
),
});
}
let nperseg = nperseg.min(x.len());
let noverlap = nperseg / 2;
let hop = nperseg - noverlap;
let segments = (x.len() - noverlap) / hop;
let window = periodic_hann(nperseg);
let energy = window.iter().fold(0.0f64, |acc, w| acc + w * w);
let factor = 1.0 / (energy / (1.0 / fs)).sqrt();
let window: Vec<f64> = window.iter().map(|w| w * factor).collect();
let mut frames = vec![0.0f64; segments * nperseg];
for (s, frame) in frames.chunks_exact_mut(nperseg).enumerate() {
let seg = &x[s * hop..s * hop + nperseg];
let mean = pairwise_mean(seg);
for ((dst, &v), &w) in frame.iter_mut().zip(seg).zip(&window) {
*dst = (v - mean) * w;
}
}
let device = CpuDevice::new();
let client = CpuClient::new(device.clone());
let input = Tensor::<CpuRuntime>::from_slice(&frames, &[segments, nperseg], &device)?;
let spectrum: Vec<Complex128> = client
.rfft(&input, FftNormalization::None)?
.contiguous()?
.try_to_vec()?;
let bins = nperseg / 2 + 1;
let paired_end = if nperseg.is_multiple_of(2) {
bins - 1
} else {
bins
};
let mut column = vec![0.0f64; segments];
let power: Vec<f64> = (0..bins)
.map(|k| {
for (s, slot) in column.iter_mut().enumerate() {
let c = spectrum[s * bins + k];
let p = c.re * c.re + c.im * c.im;
*slot = if k >= 1 && k < paired_end { p * 2.0 } else { p };
}
if segments > 1 {
pairwise_sum(&column) / segments as f64
} else {
column[0]
}
})
.collect();
let freqs = rfft_freqs(nperseg, fs);
Ok(Psd { freqs, power })
}
fn rfft_freqs(n: usize, fs: f64) -> Vec<f64> {
let val = 1.0 / (n as f64 * (1.0 / fs));
(0..n / 2 + 1).map(|k| k as f64 * val).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hann_is_periodic() {
let w = periodic_hann(8);
assert_eq!(w[0], 0.0);
assert!((w[4] - 1.0).abs() < 1e-15);
assert!((w[2] - 0.5).abs() < 1e-15 && (w[6] - 0.5).abs() < 1e-15);
}
#[test]
fn a_sine_peaks_at_its_bin_with_its_power() {
let fs = 48_000.0;
let n = 4096;
let f0 = 64.0 * fs / n as f64;
let x: Vec<f64> = (0..n * 4)
.map(|i| (2.0 * PI * f0 * i as f64 / fs).sin())
.collect();
let psd = welch(&x, fs, n).expect("welch");
assert_eq!(psd.power.len(), n / 2 + 1);
let peak = psd
.power
.iter()
.enumerate()
.max_by(|a, b| a.1.total_cmp(b.1))
.map(|(k, _)| k);
assert_eq!(peak, Some(64));
let total: f64 = psd.power.iter().sum::<f64>() * fs / n as f64;
assert!((total - 0.5).abs() < 1e-6, "power {total}");
}
#[test]
fn a_short_signal_is_one_segment() {
let x = [1.0, -1.0, 1.0, -1.0, 1.0];
let psd = welch(&x, 10.0, 4096).expect("welch");
assert_eq!(psd.freqs, vec![0.0, 2.0, 4.0]);
}
#[test]
fn empty_input_is_refused() {
assert!(welch(&[], 48_000.0, 16).is_err());
assert!(welch(&[1.0], 0.0, 16).is_err());
}
}