mod bucket;
mod decoder;
mod filter;
mod hash;
mod plan;
mod problem;
mod result;
pub use plan::SparsePlan;
pub use problem::SparseProblem;
pub use result::SparseResult;
use crate::api::Flags;
use crate::kernel::{Complex, Float};
pub fn sparse_fft<T: Float>(input: &[Complex<T>], k: usize) -> SparseResult<T> {
let n = input.len();
if n == 0 || k == 0 {
return SparseResult::empty();
}
if k >= n / 4 || n <= 64 {
return sparse_fft_fallback(input, k);
}
match SparsePlan::new(n, k, Flags::ESTIMATE) {
Some(plan) => plan.execute(input),
None => sparse_fft_fallback(input, k),
}
}
fn sparse_fft_fallback<T: Float>(input: &[Complex<T>], k: usize) -> SparseResult<T> {
use crate::api::Plan;
let n = input.len();
let plan = match Plan::dft_1d(n, crate::api::Direction::Forward, Flags::ESTIMATE) {
Some(p) => p,
None => return SparseResult::empty(),
};
let mut output = vec![Complex::<T>::zero(); n];
plan.execute(input, &mut output);
let mut magnitudes: Vec<(usize, T)> = output
.iter()
.enumerate()
.map(|(i, c)| (i, c.norm_sqr()))
.collect();
magnitudes.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(core::cmp::Ordering::Equal));
let k_actual = k.min(n);
let indices: Vec<usize> = magnitudes[..k_actual].iter().map(|(i, _)| *i).collect();
let values: Vec<Complex<T>> = indices.iter().map(|&i| output[i]).collect();
SparseResult::new(indices, values, n)
}
pub fn sparse_ifft<T: Float>(sparse_result: &SparseResult<T>, n: usize) -> Vec<Complex<T>> {
let mut output = vec![Complex::<T>::zero(); n];
let scale = T::ONE / T::from_usize(n);
let two_pi = <T as Float>::PI + <T as Float>::PI;
for t in 0..n {
let mut sum = Complex::<T>::zero();
for (&freq_idx, &value) in sparse_result
.indices
.iter()
.zip(sparse_result.values.iter())
{
let angle = two_pi * T::from_usize(freq_idx * t) / T::from_usize(n);
let (sin_a, cos_a) = Float::sin_cos(angle);
let twiddle = Complex::new(cos_a, sin_a);
sum = sum + value * twiddle;
}
output[t] = sum * scale;
}
output
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
fn build_sparse_signal(n: usize, k: usize) -> (Vec<Complex<f64>>, Vec<usize>) {
let mut signal = vec![Complex::new(0.0_f64, 0.0); n];
let two_pi = core::f64::consts::PI * 2.0;
let mut planted: Vec<usize> = (0..k)
.map(|i| {
let base = (i * (n / k.max(1))) % n;
(base + 1).min(n - 1)
})
.collect();
planted.dedup();
for &freq in &planted {
let amplitude = 1.0 + freq as f64 * 0.01; for (t, s) in signal.iter_mut().enumerate() {
let angle = two_pi * (freq as f64) * (t as f64) / (n as f64);
s.re += amplitude * angle.cos();
s.im += amplitude * angle.sin();
}
}
(signal, planted)
}
proptest! {
#[test]
fn sparse_fft_parseval(
n_log2 in 6usize..=8usize, k in 1usize..=8usize,
) {
let n = 1_usize << n_log2;
let k_clamped = k.min(n / 8);
if k_clamped == 0 {
return Ok(());
}
let (signal, _planted) = build_sparse_signal(n, k_clamped);
let signal_energy: f64 = signal.iter().map(|c| c.norm_sqr()).sum();
if signal_energy < 1e-12 {
return Ok(()); }
let result = sparse_fft(&signal, k_clamped);
let detected_energy: f64 = result.values.iter().map(|c| c.norm_sqr()).sum();
prop_assert!(
detected_energy >= 0.0,
"Detected energy should be non-negative, got {}",
detected_energy
);
for &idx in &result.indices {
prop_assert!(idx < n, "Index {} out of range [0, {})", idx, n);
}
prop_assert!(
result.indices.len() <= k_clamped,
"Returned {} components, expected at most {}",
result.indices.len(),
k_clamped
);
}
#[test]
fn sparse_fft_roundtrip(
n_log2 in 6usize..=7usize, k in 1usize..=5usize,
) {
let n = 1_usize << n_log2;
let k_clamped = k.min(n / 8).max(1);
let (signal, _planted) = build_sparse_signal(n, k_clamped);
let result = sparse_fft(&signal, k_clamped);
prop_assert!(
!result.is_empty(),
"sparse_fft returned empty result for n={}, k={}",
n,
k_clamped
);
for &idx in &result.indices {
prop_assert!(idx < n, "Index {} out of range for n={}", idx, n);
}
}
}
#[test]
fn test_sparse_fft_empty() {
let input: Vec<Complex<f64>> = vec![];
let result = sparse_fft(&input, 0);
assert!(result.is_empty());
}
#[test]
fn test_sparse_fft_single_frequency() {
let n = 256;
let freq = 10;
let mut input = vec![Complex::new(0.0_f64, 0.0); n];
let two_pi = core::f64::consts::PI * 2.0;
for i in 0..n {
let angle = two_pi * f64::from(freq) * (i as f64) / (n as f64);
input[i] = Complex::new(angle.cos(), angle.sin());
}
let result = sparse_fft(&input, 5);
assert!(!result.is_empty());
assert!(
!result.indices.is_empty(),
"Should detect at least one frequency"
);
assert!(
result.indices.iter().all(|&i| i < n),
"All indices should be valid"
);
}
#[test]
fn test_sparse_fft_fallback() {
let input = vec![Complex::new(1.0_f64, 0.0); 32];
let result = sparse_fft(&input, 8);
assert!(result.indices.len() <= 8);
}
#[test]
fn test_sparse_ifft_roundtrip() {
let n = 128;
let k = 3;
let indices = vec![5, 20, 50];
let values = vec![
Complex::new(1.0_f64, 0.0),
Complex::new(0.5, 0.5),
Complex::new(-1.0, 0.3),
];
let sparse_result = SparseResult::new(indices, values, n);
let time_signal = sparse_ifft(&sparse_result, n);
assert_eq!(time_signal.len(), n);
let recovered = sparse_fft(&time_signal, k);
assert!(!recovered.is_empty());
}
}