use std::{iter::Sum, ops::{AddAssign, DivAssign, MulAssign, RemAssign, SubAssign}};
use num_traits::{Float, ToPrimitive};
use rustfft::{num_complex::Complex, FftNum, FftPlanner};
use serde::Serialize;
use crate::analysis::FrequencyResponse;
use std::f64::consts::PI;
pub fn fft<T: FftNum + Float>(data: &[T], ts: T) -> Vec<FrequencyResponse<T>> {
let data_len: usize = data.len();
let (domega, inv_data_len) = match T::from(data.len()) {
Some(len) => {
let dfreq = T::one() / (ts * len);
let domega = T::from(2.0 * PI).unwrap() * dfreq;
(domega, T::one() / len)
},
None => return vec![],
};
let mut planner: FftPlanner<T> = FftPlanner::new();
let fft = planner.plan_fft_forward(data_len);
let mut buffer: Vec<Complex<T>> = data
.iter()
.map(|&x| Complex::from(x * inv_data_len))
.collect();
fft.process(&mut buffer);
let data_len_half: usize = buffer.len() / 2;
buffer[0..data_len_half]
.iter()
.enumerate()
.map(|(i, &value)| {
let omega = T::from(i).unwrap() * domega;
FrequencyResponse { omega, value }
})
.collect()
}
#[derive(Copy, Clone, Debug, Serialize)]
pub struct Spectrum<T> {
pub omega: T,
pub value: T,
}
pub fn amplitude_spectrum<T: FftNum + Float>(data: &[T], ts: T) -> Vec<Spectrum<T>> {
let n = data.len();
if n == 0 {
return vec![];
}
let len = T::from(n).unwrap();
let domega = T::from(2.0 * PI).unwrap() / (ts * len);
let mut planner: FftPlanner<T> = FftPlanner::new();
let fft = planner.plan_fft_forward(n);
let mut buffer: Vec<Complex<T>> = data.iter().map(|&x| Complex::from(x)).collect();
fft.process(&mut buffer);
let two = T::from(2.0).unwrap();
(0..=n / 2)
.map(|k| {
let amplitude = buffer[k].norm() / len;
let value = if is_one_sided_edge(k, n) { amplitude } else { two * amplitude };
Spectrum { omega: T::from(k).unwrap() * domega, value }
})
.collect()
}
pub fn power_spectral_density<T: FftNum + Float + Sum + AddAssign + SubAssign + DivAssign + MulAssign + RemAssign + ToPrimitive>(
data: &[T],
ts: T,
n_segments: usize,
) -> Vec<Spectrum<T>> {
if data.len() < 2 {
return vec![];
}
let mut psd: Vec<T> = vec![];
let nperseg = welch_segment_len(data.len(), n_segments);
let segments = welch_segments([data], ts, nperseg, |[x], scale| {
psd.resize(x.len() / 2 + 1, T::zero());
for (p, xk) in psd.iter_mut().zip(x) {
*p += xk.norm_sqr() * scale;
}
});
let averager = T::one() / T::from(segments.count).unwrap();
let two = T::from(2.0).unwrap();
psd.into_iter()
.enumerate()
.map(|(k, p)| {
let p = p * averager;
let value = if is_one_sided_edge(k, segments.len) { p } else { two * p };
Spectrum { omega: segments.omega(k), value }
})
.collect()
}
#[derive(Clone, Debug, Serialize)]
pub struct Spectrogram<T> {
pub time: Vec<T>,
pub omega: Vec<T>,
pub psd: Vec<Vec<T>>,
}
pub fn spectrogram<T: FftNum + Float + Sum + AddAssign + DivAssign + ToPrimitive>(
data: &[T],
ts: T,
window_time: T,
) -> Spectrogram<T> {
let samples = window_time / ts;
assert!(samples > T::zero() && samples.is_finite(), "spectrogram: window_time / ts must be positive and finite");
if data.len() < 2 {
return Spectrogram { time: vec![], omega: vec![], psd: vec![] };
}
let nperseg = samples.round().to_usize().unwrap_or(usize::MAX).clamp(2, data.len());
let two = T::from(2.0).unwrap();
let mut psd: Vec<Vec<T>> = vec![];
let segments = welch_segments([data], ts, nperseg, |[x], scale| {
psd.push(
x[..=nperseg / 2]
.iter()
.enumerate()
.map(|(k, xk)| {
let p = xk.norm_sqr() * scale;
if is_one_sided_edge(k, nperseg) { p } else { two * p }
})
.collect(),
);
});
let step = nperseg - nperseg / 2;
let time = (0..segments.count)
.map(|i| T::from(i * step).unwrap() * ts + T::from(nperseg).unwrap() / two * ts)
.collect();
let omega = (0..=nperseg / 2).map(|k| segments.omega(k)).collect();
Spectrogram { time, omega, psd }
}
fn is_one_sided_edge(k: usize, n: usize) -> bool {
k == 0 || (n.is_multiple_of(2) && k == n / 2)
}
pub fn welch<T: FftNum + Float + Sum + AddAssign + SubAssign + DivAssign + MulAssign + RemAssign + ToPrimitive>(
u: &[T],
y: &[T],
ts: T,
n_segments: usize,
) -> (Vec<FrequencyResponse<T>>, Vec<T>) {
assert_eq!(u.len(), y.len(), "welch: input and output lengths differ");
if u.len() < 2 {
return (vec![], vec![]);
}
let mut puu: Vec<T> = vec![];
let mut pyy: Vec<T> = vec![];
let mut pyu: Vec<Complex<T>> = vec![];
let nperseg = welch_segment_len(u.len(), n_segments);
let segments = welch_segments([u, y], ts, nperseg, |[bu, by], scale| {
let bins = bu.len() / 2 + 1;
puu.resize(bins, T::zero());
pyy.resize(bins, T::zero());
pyu.resize(bins, Complex::from(T::zero()));
for k in 0..bins {
let uu = bu[k].norm_sqr() * scale;
let yy = by[k].norm_sqr() * scale;
let yu = by[k] * bu[k].conj() * scale;
puu[k] += uu;
pyy[k] += yy;
pyu[k] += yu;
}
});
let bins = segments.len / 2 + 1;
let averager = T::one() / T::from(segments.count).unwrap();
for k in 0..bins {
puu[k] *= averager;
pyy[k] *= averager;
pyu[k] *= averager;
}
let mut ret: Vec<FrequencyResponse<T>> = Vec::with_capacity(bins);
let mut coherence = vec![T::zero(); bins];
let (floor_u, floor_y) = (power_floor(&puu), power_floor(&pyy));
for k in 0..bins {
let omega = segments.omega(k);
let value = if puu[k] > floor_u { pyu[k] / puu[k] } else { Complex::from(T::zero()) };
ret.push(FrequencyResponse { omega, value });
coherence[k] = coherence_of(pyu[k], puu[k], pyy[k], floor_u, floor_y);
}
(ret, coherence)
}
struct WelchSegments<T> {
len: usize,
count: usize,
fs: T,
}
impl<T: Float> WelchSegments<T> {
fn omega(&self, k: usize) -> T {
T::from(2.0 * PI).unwrap() * T::from(k).unwrap() * self.fs / T::from(self.len).unwrap()
}
}
fn welch_segment_len(n: usize, n_segments: usize) -> usize {
n.div_ceil(n_segments.max(1)).next_power_of_two().clamp(2, n)
}
pub(crate) fn hann_window<T: Float>(len: usize) -> Vec<T> {
(0..len).map(|i| {
T::from(0.5).unwrap() * (T::one() - (T::from(2.0 * PI).unwrap() * T::from(i).unwrap() / T::from(len).unwrap()).cos())
}).collect()
}
fn welch_segments<T, const M: usize>(
signals: [&[T]; M],
ts: T,
nperseg: usize,
mut accumulate: impl FnMut(&[Vec<Complex<T>>; M], T),
) -> WelchSegments<T>
where
T: FftNum + Float + Sum + AddAssign + DivAssign,
{
let n = signals[0].len();
let fs = T::one() / ts;
let noverlap = nperseg / 2;
let window = hann_window::<T>(nperseg);
let scale = T::one() / (fs * window.iter().map(|&w| w*w).sum::<T>());
let mut planner = FftPlanner::new();
let fft = planner.plan_fft_forward(nperseg);
let step = nperseg - noverlap;
let mut num_segments = 0;
let mut buffers: [Vec<Complex<T>>; M] = std::array::from_fn(|_| vec![Complex::from(T::zero()); nperseg]);
for start in (0..=n - nperseg).step_by(step) {
for (signal, buffer) in signals.iter().zip(buffers.iter_mut()) {
let segment = &signal[start..start + nperseg];
let mut mean = T::zero();
for &x in segment {
mean += x;
}
mean /= T::from(nperseg).unwrap();
for ((b, &x), &w) in buffer.iter_mut().zip(segment).zip(&window) {
*b = Complex::from((x - mean) * w);
}
fft.process(buffer);
}
accumulate(&buffers, scale);
num_segments += 1;
}
WelchSegments { len: nperseg, count: num_segments, fs }
}
pub(crate) fn power_floor<T: Float>(p: &[T]) -> T {
T::from(1e-12).unwrap() * p.iter().copied().fold(T::zero(), T::max)
}
pub(crate) fn coherence_of<T: Float>(ab: Complex<T>, aa: T, bb: T, floor_a: T, floor_b: T) -> T {
if aa > floor_a && bb > floor_b {
let c = ab.norm() / aa.sqrt() / bb.sqrt();
c * c
} else {
T::zero()
}
}