//! Utility functions for connectivity analysis. use num_complex::Complex64; use rustfft::{FftDirection, FftPlanner}; /// Compute FFT frequencies for a given sample rate pub fn fft_freqs(sfreq: f64, n_fft: usize, fmin: f64, fmax: f64) -> Vec { let freq_resolution = sfreq / n_fft as f64; let n_freqs = n_fft / 2 + 1; (0..n_freqs) .map(|i| i as f64 * freq_resolution) .filter(|&f| f >= fmin && f <= fmax) .collect() } /// Get frequency indices for a given range pub fn freq_indices(sfreq: f64, n_fft: usize, fmin: f64, fmax: f64) -> Vec { let freq_resolution = sfreq / n_fft as f64; let n_freqs = n_fft / 2 + 1; (0..n_freqs) .filter(|&i| { let f = i as f64 * freq_resolution; f >= fmin && f <= fmax }) .collect() } /// Compute FFT of a real signal pub fn rfft(signal: &[f64], n_fft: usize) -> Vec { let mut planner = FftPlanner::new(); let fft = planner.plan_fft(n_fft, FftDirection::Forward); // Zero-pad signal let mut input: Vec = signal .iter() .map(|&x| Complex64::new(x, 0.0)) .chain(std::iter::repeat(Complex64::new(0.0, 0.0))) .take(n_fft) .collect(); fft.process(&mut input); // Return only positive frequencies input.truncate(n_fft / 2 + 1); input } /// Compute cross-spectral density pub fn cross_spectral_density(x: &[Complex64], y: &[Complex64]) -> Vec { x.iter().zip(y).map(|(a, b)| a * b.conj()).collect() } /// Compute power spectral density pub fn power_spectral_density(x: &[Complex64]) -> Vec { x.iter().map(num_complex::Complex::norm_sqr).collect() } /// Apply Hanning window pub fn hanning_window(n: usize) -> Vec { use std::f64::consts::PI; (0..n) .map(|i| 0.5 * (1.0 - (2.0 * PI * i as f64 / (n - 1) as f64).cos())) .collect() } /// Apply window to signal pub fn apply_window(signal: &[f64], window: &[f64]) -> Vec { signal.iter().zip(window).map(|(&s, &w)| s * w).collect() } /// Hilbert transform to get analytic signal pub fn hilbert(signal: &[f64]) -> Vec { let n = signal.len(); let mut planner = FftPlanner::new(); let fft = planner.plan_fft(n, FftDirection::Forward); let ifft = planner.plan_fft(n, FftDirection::Inverse); // Forward FFT let mut spectrum: Vec = signal.iter().map(|&x| Complex64::new(x, 0.0)).collect(); fft.process(&mut spectrum); // Create analytic signal in frequency domain // H(f) = 2 for f > 0, H(0) = 1, H(f) = 0 for f < 0 let half = (n + 1) / 2; for i in 1..half { spectrum[i] *= 2.0; } for i in half..n { spectrum[i] = Complex64::new(0.0, 0.0); } // Inverse FFT ifft.process(&mut spectrum); // Normalize let scale = 1.0 / n as f64; spectrum.iter_mut().for_each(|c| *c *= scale); spectrum } /// Extract instantaneous phase from analytic signal pub fn instantaneous_phase(analytic: &[Complex64]) -> Vec { analytic.iter().map(|c| c.arg()).collect() } /// Bandpass filter using FFT pub fn bandpass_fft(signal: &[f64], sfreq: f64, fmin: f64, fmax: f64) -> Vec { let n = signal.len(); let mut planner = FftPlanner::new(); let fft = planner.plan_fft(n, FftDirection::Forward); let ifft = planner.plan_fft(n, FftDirection::Inverse); // Forward FFT let mut spectrum: Vec = signal.iter().map(|&x| Complex64::new(x, 0.0)).collect(); fft.process(&mut spectrum); // Apply bandpass let freq_resolution = sfreq / n as f64; for i in 0..n { let freq = if i <= n / 2 { i as f64 * freq_resolution } else { (n - i) as f64 * freq_resolution }; if freq < fmin || freq > fmax { spectrum[i] = Complex64::new(0.0, 0.0); } } // Inverse FFT ifft.process(&mut spectrum); // Return real part, normalized let scale = 1.0 / n as f64; spectrum.iter().map(|c| c.re * scale).collect() } #[cfg(test)] mod tests { use super::*; use std::f64::consts::PI; #[test] fn test_fft_freqs() { let freqs = fft_freqs(100.0, 100, 0.0, 50.0); assert_eq!(freqs[0], 0.0); assert_eq!(freqs[1], 1.0); assert_eq!(freqs.len(), 51); } #[test] fn test_hanning_window() { let window = hanning_window(5); assert!((window[0] - 0.0).abs() < 1e-10); assert!((window[2] - 1.0).abs() < 1e-10); assert!((window[4] - 0.0).abs() < 1e-10); } #[test] fn test_rfft() { // Test with simple sinusoid let n = 100; let signal: Vec = (0..n) .map(|i| (2.0 * PI * 10.0 * i as f64 / n as f64).sin()) .collect(); let spectrum = rfft(&signal, n); // Peak should be at index 10 let magnitudes: Vec = spectrum.iter().map(|c| c.norm()).collect(); let peak_idx = magnitudes .iter() .enumerate() .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap()) .map(|(i, _)| i) .unwrap(); assert_eq!(peak_idx, 10); } #[test] fn test_hilbert() { let n = 100; let signal: Vec = (0..n) .map(|i| (2.0 * PI * 5.0 * i as f64 / n as f64).sin()) .collect(); let analytic = hilbert(&signal); // Magnitude should be approximately constant (envelope of pure sinusoid) let magnitudes: Vec = analytic.iter().map(|c| c.norm()).collect(); let mean_mag = magnitudes.iter().sum::() / n as f64; // Should be close to 1 assert!((mean_mag - 1.0).abs() < 0.2); } }