use scirs2_core::ndarray::{s, Array1, Array2, Axis};
use sklears_core::{
error::{Result, SklearsError},
types::Float,
};
use std::f64::consts::PI;
#[derive(Debug, Clone)]
pub struct SpectralDecomposition {
pub window_size: usize,
pub overlap: usize,
pub window_function: WindowFunction,
pub zero_padding_factor: usize,
pub sampling_rate: Float,
}
#[derive(Debug, Clone, Copy)]
pub enum WindowFunction {
Rectangular,
Hanning,
Hamming,
Blackman,
Kaiser(Float),
Tukey(Float),
Gaussian(Float),
Bartlett,
BlackmanHarris,
}
impl SpectralDecomposition {
pub fn new(window_size: usize) -> Self {
Self {
window_size,
overlap: window_size / 2,
window_function: WindowFunction::Hanning,
zero_padding_factor: 1,
sampling_rate: 1.0,
}
}
pub fn overlap(mut self, overlap: usize) -> Self {
self.overlap = overlap;
self
}
pub fn window_function(mut self, window_function: WindowFunction) -> Self {
self.window_function = window_function;
self
}
pub fn zero_padding_factor(mut self, factor: usize) -> Self {
self.zero_padding_factor = factor;
self
}
pub fn sampling_rate(mut self, sampling_rate: Float) -> Self {
self.sampling_rate = sampling_rate;
self
}
pub fn stft(&self, signal: &Array1<Float>) -> Result<SpectralResult> {
self.validate_parameters(signal)?;
let n = signal.len();
let hop_size = self.window_size - self.overlap;
let n_frames = (n - self.overlap) / hop_size;
let fft_size = self.window_size * self.zero_padding_factor;
let n_freq_bins = fft_size / 2 + 1;
let mut magnitude = Array2::zeros((n_freq_bins, n_frames));
let mut phase = Array2::zeros((n_freq_bins, n_frames));
let window = self.generate_window()?;
for frame in 0..n_frames {
let start = frame * hop_size;
let end = (start + self.window_size).min(n);
if end - start < self.window_size {
break;
}
let mut windowed_signal = Array1::zeros(fft_size);
for i in 0..self.window_size {
if start + i < n {
windowed_signal[i] = signal[start + i] * window[i];
}
}
let (mag, ph) = self.compute_dft(&windowed_signal)?;
for freq in 0..n_freq_bins {
magnitude[[freq, frame]] = mag[freq];
phase[[freq, frame]] = ph[freq];
}
}
Ok(SpectralResult {
magnitude,
phase,
frequencies: self.generate_frequency_bins(),
times: self.generate_time_bins(hop_size as Float, n_frames),
sampling_rate: self.sampling_rate,
window_function: self.window_function,
})
}
pub fn cross_spectral_analysis(&self, signals: &Array2<Float>) -> Result<CrossSpectralResult> {
let (n_channels, _signal_length) = signals.dim();
if n_channels < 2 {
return Err(SklearsError::InvalidInput(
"Cross-spectral analysis requires at least 2 channels".to_string(),
));
}
let mut channel_stfts = Vec::new();
for ch in 0..n_channels {
let signal = signals.slice(s![ch, ..]).to_owned();
let stft_result = self.stft(&signal)?;
channel_stfts.push(stft_result);
}
let (n_freq, n_time) = channel_stfts[0].magnitude.dim();
let mut cross_spectrum = Array2::zeros((n_freq, n_time));
let mut coherence = Array2::zeros((n_freq, n_time));
let mut phase_difference = Array2::zeros((n_freq, n_time));
for freq in 0..n_freq {
for time in 0..n_time {
let mag1 = channel_stfts[0].magnitude[[freq, time]];
let mag2 = channel_stfts[1].magnitude[[freq, time]];
let phase1 = channel_stfts[0].phase[[freq, time]];
let phase2 = channel_stfts[1].phase[[freq, time]];
cross_spectrum[[freq, time]] = mag1 * mag2;
coherence[[freq, time]] = if mag1 > 1e-12 && mag2 > 1e-12 {
(cross_spectrum[[freq, time]] / (mag1 * mag1 + mag2 * mag2)).min(1.0)
} else {
0.0
};
phase_difference[[freq, time]] = phase1 - phase2;
}
}
Ok(CrossSpectralResult {
cross_spectrum,
coherence,
phase_difference,
frequencies: channel_stfts[0].frequencies.clone(),
times: channel_stfts[0].times.clone(),
n_channels,
})
}
fn validate_parameters(&self, signal: &Array1<Float>) -> Result<()> {
if signal.is_empty() {
return Err(SklearsError::InvalidInput("Empty signal".to_string()));
}
if self.window_size == 0 {
return Err(SklearsError::InvalidInput(
"Window size must be positive".to_string(),
));
}
if self.overlap >= self.window_size {
return Err(SklearsError::InvalidInput(
"Overlap must be less than window size".to_string(),
));
}
if signal.len() < self.window_size {
return Err(SklearsError::InvalidInput(
"Signal must be at least as long as window size".to_string(),
));
}
if self.zero_padding_factor == 0 {
return Err(SklearsError::InvalidInput(
"Zero padding factor must be positive".to_string(),
));
}
Ok(())
}
fn generate_window(&self) -> Result<Array1<Float>> {
let mut window = Array1::zeros(self.window_size);
match self.window_function {
WindowFunction::Rectangular => {
window.fill(1.0);
}
WindowFunction::Hanning => {
for i in 0..self.window_size {
let phase = 2.0 * PI * i as Float / (self.window_size - 1) as Float;
window[i] = 0.5 * (1.0 - phase.cos());
}
}
WindowFunction::Hamming => {
for i in 0..self.window_size {
let phase = 2.0 * PI * i as Float / (self.window_size - 1) as Float;
window[i] = 0.54 - 0.46 * phase.cos();
}
}
WindowFunction::Blackman => {
for i in 0..self.window_size {
let phase = 2.0 * PI * i as Float / (self.window_size - 1) as Float;
window[i] = 0.42 - 0.5 * phase.cos() + 0.08 * (2.0 * phase).cos();
}
}
WindowFunction::Kaiser(beta) => {
let beta = beta.max(0.0);
let norm = self.modified_bessel_i0(beta);
for i in 0..self.window_size {
let x = 2.0 * i as Float / (self.window_size - 1) as Float - 1.0;
let arg = beta * (1.0 - x * x).max(0.0).sqrt();
window[i] = self.modified_bessel_i0(arg) / norm;
}
}
WindowFunction::Tukey(alpha) => {
let alpha = alpha.clamp(0.0, 1.0);
let n_taper = (alpha * self.window_size as Float / 2.0) as usize;
for i in 0..self.window_size {
if i < n_taper {
let phase = PI * i as Float / n_taper as Float;
window[i] = 0.5 * (1.0 - phase.cos());
} else if i >= self.window_size - n_taper {
let phase = PI * (self.window_size - 1 - i) as Float / n_taper as Float;
window[i] = 0.5 * (1.0 - phase.cos());
} else {
window[i] = 1.0;
}
}
}
WindowFunction::Gaussian(sigma) => {
let sigma = sigma.max(0.1);
let center = (self.window_size - 1) as Float / 2.0;
for i in 0..self.window_size {
let x = (i as Float - center) / (sigma * center);
window[i] = (-0.5 * x * x).exp();
}
}
WindowFunction::Bartlett => {
let n_half = (self.window_size - 1) as Float / 2.0;
for i in 0..self.window_size {
window[i] = 1.0 - (i as Float - n_half).abs() / n_half;
}
}
WindowFunction::BlackmanHarris => {
for i in 0..self.window_size {
let phase = 2.0 * PI * i as Float / (self.window_size - 1) as Float;
window[i] = 0.35875 - 0.48829 * phase.cos() + 0.14128 * (2.0 * phase).cos()
- 0.01168 * (3.0 * phase).cos();
}
}
}
Ok(window)
}
fn modified_bessel_i0(&self, x: Float) -> Float {
let mut result = 1.0;
let mut term = 1.0;
let x_half = x / 2.0;
for n in 1..50 {
term *= x_half * x_half / (n * n) as Float;
result += term;
if term < 1e-12 {
break;
}
}
result
}
fn compute_dft(&self, signal: &Array1<Float>) -> Result<(Array1<Float>, Array1<Float>)> {
let n = signal.len();
let n_freq = n / 2 + 1;
let mut magnitude = Array1::zeros(n_freq);
let mut phase = Array1::zeros(n_freq);
for k in 0..n_freq {
let mut real = 0.0;
let mut imag = 0.0;
for n_idx in 0..n {
let angle = -2.0 * PI * k as Float * n_idx as Float / n as Float;
real += signal[n_idx] * angle.cos();
imag += signal[n_idx] * angle.sin();
}
magnitude[k] = (real * real + imag * imag).sqrt();
phase[k] = imag.atan2(real);
}
Ok((magnitude, phase))
}
fn generate_frequency_bins(&self) -> Array1<Float> {
let fft_size = self.window_size * self.zero_padding_factor;
let n_freq = fft_size / 2 + 1;
let mut frequencies = Array1::zeros(n_freq);
for i in 0..n_freq {
frequencies[i] = i as Float * self.sampling_rate / fft_size as Float;
}
frequencies
}
fn generate_time_bins(&self, hop_size: Float, n_frames: usize) -> Array1<Float> {
let mut times = Array1::zeros(n_frames);
for i in 0..n_frames {
times[i] = i as Float * hop_size / self.sampling_rate;
}
times
}
}
#[derive(Debug, Clone)]
pub struct SpectralResult {
pub magnitude: Array2<Float>,
pub phase: Array2<Float>,
pub frequencies: Array1<Float>,
pub times: Array1<Float>,
pub sampling_rate: Float,
pub window_function: WindowFunction,
}
impl SpectralResult {
pub fn power_spectral_density(&self) -> Array2<Float> {
self.magnitude.mapv(|x| x * x)
}
pub fn spectral_peaks(&self, threshold: Float) -> Vec<Vec<(usize, Float)>> {
let (n_freq, n_frames) = self.magnitude.dim();
let mut peaks = Vec::new();
for frame in 0..n_frames {
let mut frame_peaks = Vec::new();
for freq in 1..n_freq - 1 {
let current = self.magnitude[[freq, frame]];
let prev = self.magnitude[[freq - 1, frame]];
let next = self.magnitude[[freq + 1, frame]];
if current > prev && current > next && current > threshold {
frame_peaks.push((freq, current));
}
}
peaks.push(frame_peaks);
}
peaks
}
pub fn spectral_centroid(&self) -> Array1<Float> {
let (n_freq, n_frames) = self.magnitude.dim();
let mut centroids = Array1::zeros(n_frames);
for frame in 0..n_frames {
let mut weighted_sum = 0.0;
let mut magnitude_sum = 0.0;
for freq in 0..n_freq {
let mag = self.magnitude[[freq, frame]];
weighted_sum += self.frequencies[freq] * mag;
magnitude_sum += mag;
}
centroids[frame] = if magnitude_sum > 1e-12 {
weighted_sum / magnitude_sum
} else {
0.0
};
}
centroids
}
pub fn spectral_rolloff(&self, rolloff_percentage: Float) -> Array1<Float> {
let (n_freq, n_frames) = self.magnitude.dim();
let mut rolloffs = Array1::zeros(n_frames);
let threshold = rolloff_percentage.clamp(0.0, 1.0);
for frame in 0..n_frames {
let psd = self.magnitude.slice(s![.., frame]).mapv(|x| x * x);
let total_energy: Float = psd.sum();
let target_energy = total_energy * threshold;
let mut cumulative_energy = 0.0;
let mut rolloff_freq = self.frequencies[n_freq - 1];
for freq in 0..n_freq {
cumulative_energy += psd[freq];
if cumulative_energy >= target_energy {
rolloff_freq = self.frequencies[freq];
break;
}
}
rolloffs[frame] = rolloff_freq;
}
rolloffs
}
}
#[derive(Debug, Clone)]
pub struct CrossSpectralResult {
pub cross_spectrum: Array2<Float>,
pub coherence: Array2<Float>,
pub phase_difference: Array2<Float>,
pub frequencies: Array1<Float>,
pub times: Array1<Float>,
pub n_channels: usize,
}
impl CrossSpectralResult {
pub fn average_coherence(&self) -> Array1<Float> {
self.coherence
.mean_axis(Axis(1))
.expect("array should have elements for mean computation")
}
pub fn high_coherence_frequencies(&self, coherence_threshold: Float) -> Vec<usize> {
let avg_coherence = self.average_coherence();
avg_coherence
.iter()
.enumerate()
.filter_map(|(idx, &coh)| {
if coh > coherence_threshold {
Some(idx)
} else {
None
}
})
.collect()
}
}
#[allow(non_snake_case)]
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_spectral_decomposition_creation() {
let spectral = SpectralDecomposition::new(128)
.overlap(64)
.window_function(WindowFunction::Hanning)
.sampling_rate(44100.0);
assert_eq!(spectral.window_size, 128);
assert_eq!(spectral.overlap, 64);
assert_eq!(spectral.sampling_rate, 44100.0);
}
#[test]
fn test_window_functions() {
let spectral = SpectralDecomposition::new(32);
let window_functions = vec![
WindowFunction::Rectangular,
WindowFunction::Hanning,
WindowFunction::Hamming,
WindowFunction::Blackman,
WindowFunction::Kaiser(2.0),
WindowFunction::Tukey(0.5),
WindowFunction::Gaussian(0.4),
WindowFunction::Bartlett,
WindowFunction::BlackmanHarris,
];
for window_func in window_functions {
let spectral_with_window = spectral.clone().window_function(window_func);
let window = spectral_with_window
.generate_window()
.expect("operation should succeed");
assert_eq!(window.len(), 32);
assert!(!window.iter().any(|&x| x.is_nan() || x.is_infinite()));
}
}
#[test]
fn test_stft_basic_functionality() {
let spectral = SpectralDecomposition::new(32)
.overlap(16)
.sampling_rate(1000.0);
let mut signal = Array1::zeros(128);
for i in 0..128 {
signal[i] = (2.0 * PI * 10.0 * i as Float / 1000.0).sin();
}
let result = spectral.stft(&signal).expect("operation should succeed");
assert_eq!(result.magnitude.nrows(), 17); assert!(result.magnitude.ncols() > 0);
assert_eq!(result.phase.dim(), result.magnitude.dim());
assert_eq!(result.frequencies.len(), result.magnitude.nrows());
assert_eq!(result.times.len(), result.magnitude.ncols());
}
#[test]
fn test_power_spectral_density() {
let spectral = SpectralDecomposition::new(16);
let signal = Array1::ones(64);
let result = spectral.stft(&signal).expect("operation should succeed");
let psd = result.power_spectral_density();
assert_eq!(psd.dim(), result.magnitude.dim());
assert!(psd.iter().all(|&x| x >= 0.0));
}
#[test]
fn test_spectral_peaks() {
let spectral = SpectralDecomposition::new(16);
let mut signal = Array1::zeros(64);
for i in 0..64 {
signal[i] = (2.0 * PI * 2.0 * i as Float / 16.0).sin() + 0.1;
}
let result = spectral.stft(&signal).expect("operation should succeed");
let peaks = result.spectral_peaks(0.5);
assert_eq!(peaks.len(), result.magnitude.ncols());
}
#[test]
fn test_spectral_centroid() {
let spectral = SpectralDecomposition::new(16).sampling_rate(1000.0);
let signal = Array1::ones(64);
let result = spectral.stft(&signal).expect("operation should succeed");
let centroids = result.spectral_centroid();
assert_eq!(centroids.len(), result.magnitude.ncols());
assert!(centroids.iter().all(|&x| (0.0..=500.0).contains(&x))); }
#[test]
fn test_cross_spectral_analysis() {
let spectral = SpectralDecomposition::new(16);
let mut signals = Array2::zeros((2, 64));
for i in 0..64 {
signals[[0, i]] = (2.0 * PI * i as Float / 16.0).sin();
signals[[1, i]] = (2.0 * PI * i as Float / 16.0).cos(); }
let result = spectral
.cross_spectral_analysis(&signals)
.expect("operation should succeed");
assert_eq!(result.n_channels, 2);
assert_eq!(result.cross_spectrum.nrows(), 9); assert_eq!(result.coherence.dim(), result.cross_spectrum.dim());
assert_eq!(result.phase_difference.dim(), result.cross_spectrum.dim());
}
#[test]
fn test_error_handling() {
let spectral = SpectralDecomposition::new(32);
let empty_signal = Array1::zeros(0);
assert!(spectral.stft(&empty_signal).is_err());
let short_signal = Array1::zeros(16); assert!(spectral.stft(&short_signal).is_err());
let invalid_spectral = SpectralDecomposition::new(32).overlap(32);
let signal = Array1::zeros(64);
assert!(invalid_spectral.stft(&signal).is_err());
}
#[test]
fn test_kaiser_window_properties() {
let spectral = SpectralDecomposition::new(32).window_function(WindowFunction::Kaiser(2.0));
let window = spectral
.generate_window()
.expect("operation should succeed");
let center = window.len() / 2;
assert!(window[center] >= window[0]);
assert!(window[center] >= window[window.len() - 1]);
}
#[test]
fn test_spectral_rolloff() {
let spectral = SpectralDecomposition::new(16).sampling_rate(1000.0);
let signal = Array1::ones(64);
let result = spectral.stft(&signal).expect("operation should succeed");
let rolloffs = result.spectral_rolloff(0.85);
assert_eq!(rolloffs.len(), result.magnitude.ncols());
assert!(rolloffs.iter().all(|&x| (0.0..=500.0).contains(&x)));
}
}