mod bucket;
mod decoder;
mod filter;
mod hash;
mod plan;
mod problem;
mod result;
pub use decoder::{detect_singleton, is_collision, PeelingDecoder};
pub use filter::{create_optimal_filter, AliasingFilter, FilterType};
pub use hash::{generate_coprime_factors, CrtHash, FrequencyHash};
pub use plan::SparsePlan;
pub use problem::SparseProblem;
pub use result::SparseResult;
use crate::api::Flags;
use crate::kernel::{Complex, Float};
use crate::prelude::*;
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_fft_auto<T: Float>(input: &[Complex<T>]) -> SparseResult<T> {
sparse_fft_auto_with_ratio(input, T::from_f64(0.99))
}
pub fn sparse_fft_auto_with_ratio<T: Float>(
input: &[Complex<T>],
energy_ratio: T,
) -> SparseResult<T> {
let n = input.len();
if n == 0 {
return SparseResult::empty();
}
use crate::api::Plan;
let plan = match Plan::dft_1d(n, crate::api::Direction::Forward, Flags::ESTIMATE) {
Some(p) => p,
None => return SparseResult::empty(),
};
let mut spectrum = vec![Complex::<T>::zero(); n];
plan.execute(input, &mut spectrum);
let mut mag_vec: Vec<(usize, T)> = spectrum
.iter()
.enumerate()
.map(|(i, c)| (i, c.norm_sqr()))
.collect();
let total_energy: T = mag_vec.iter().map(|(_, m)| *m).fold(T::ZERO, |a, b| a + b);
if total_energy <= T::ZERO {
return SparseResult::empty();
}
mag_vec.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(core::cmp::Ordering::Equal));
let target = total_energy * energy_ratio;
let mut accumulated = T::ZERO;
let mut estimated_k: usize = 0;
for &(_, mag) in &mag_vec {
accumulated = accumulated + mag;
estimated_k += 1;
if accumulated >= target {
break;
}
}
let estimated_k = estimated_k.max(1).min(n);
let top_k = &mag_vec[..estimated_k];
let indices: Vec<usize> = top_k.iter().map(|(i, _)| *i).collect();
let values: Vec<Complex<T>> = indices.iter().map(|&i| spectrum[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());
}
#[test]
fn test_sparse_fft_k_zero() {
let input = vec![Complex::new(1.0_f64, 0.0); 256];
let result = sparse_fft(&input, 0);
assert!(result.is_empty(), "k=0 must yield empty result");
}
#[test]
fn test_sparse_fft_k_one() {
let n = 256;
let mut input = vec![Complex::new(0.0_f64, 0.0); n];
let two_pi = core::f64::consts::PI * 2.0;
let freq = 17;
for i in 0..n {
let angle = two_pi * (freq as f64) * (i as f64) / (n as f64);
input[i] = Complex::new(angle.cos(), angle.sin());
}
let result = sparse_fft(&input, 1);
assert!(result.len() <= 1, "k=1 should return at most 1 component");
for &idx in &result.indices {
assert!(idx < n);
}
}
#[test]
fn test_sparse_fft_k_half_dense() {
let n = 128;
let k = n / 2;
let input = vec![Complex::new(1.0_f64, 0.0); n];
let result = sparse_fft(&input, k);
assert!(result.indices.len() <= k, "Result should not exceed k={k}");
for &idx in &result.indices {
assert!(idx < n);
}
}
#[test]
fn test_sparse_fft_noise_signal() {
let n = 256;
let mut input = vec![Complex::new(0.0_f64, 0.0); n];
for freq in 0..n {
let angle = core::f64::consts::PI * (freq as f64) / (n as f64);
let tiny = 1e-6 * angle.sin();
input[freq] = Complex::new(tiny, tiny);
}
let result = sparse_fft(&input, 8);
for &idx in &result.indices {
assert!(idx < n);
}
}
#[test]
fn test_sparse_fft_k_equals_n() {
let n = 64;
let mut input = vec![Complex::new(0.0_f64, 0.0); n];
let two_pi = core::f64::consts::PI * 2.0;
for freq in 0..n {
for t in 0..n {
let angle = two_pi * (freq as f64) * (t as f64) / (n as f64);
input[t].re += angle.cos() / (n as f64);
input[t].im += angle.sin() / (n as f64);
}
}
let result = sparse_fft(&input, n);
assert!(result.indices.len() <= n);
for &idx in &result.indices {
assert!(idx < n);
}
}
#[test]
fn test_sparse_fft_auto_empty() {
let input: Vec<Complex<f64>> = vec![];
let result = sparse_fft_auto(&input);
assert!(result.is_empty());
}
#[test]
fn test_sparse_fft_auto_single_freq() {
let n = 256;
let mut input = vec![Complex::new(0.0_f64, 0.0); n];
let two_pi = core::f64::consts::PI * 2.0;
let freq = 10;
for i in 0..n {
let angle = two_pi * (freq as f64) * (i as f64) / (n as f64);
input[i] = Complex::new(angle.cos(), angle.sin());
}
let result = sparse_fft_auto(&input);
assert!(!result.is_empty(), "Auto should detect a clear single tone");
let sorted = result.sorted_by_magnitude();
assert_eq!(sorted[0].0, freq, "Top frequency should be the planted one");
}
#[test]
fn test_sparse_fft_auto_multiple_freqs() {
let n = 512;
let mut input = vec![Complex::new(0.0_f64, 0.0); n];
let two_pi = core::f64::consts::PI * 2.0;
let planted = [5usize, 50, 200];
for &freq in &planted {
for i in 0..n {
let angle = two_pi * (freq as f64) * (i as f64) / (n as f64);
input[i].re += angle.cos();
input[i].im += angle.sin();
}
}
let result = sparse_fft_auto(&input);
assert!(!result.is_empty(), "Auto should detect planted frequencies");
for &freq in &planted {
assert!(
result.indices.contains(&freq),
"Planted frequency {freq} not found in result"
);
}
}
#[test]
fn test_sparse_fft_auto_custom_ratio() {
let n = 256;
let mut input = vec![Complex::new(0.0_f64, 0.0); n];
let two_pi = core::f64::consts::PI * 2.0;
let planted = [(10usize, 10.0_f64), (50, 1.0), (100, 0.5)];
for &(freq, amp) in &planted {
for i in 0..n {
let angle = two_pi * (freq as f64) * (i as f64) / (n as f64);
input[i].re += amp * angle.cos();
input[i].im += amp * angle.sin();
}
}
let tight = sparse_fft_auto_with_ratio(&input, 0.90);
let loose = sparse_fft_auto_with_ratio(&input, 0.99);
assert!(
tight.len() <= loose.len(),
"Tight ratio ({}) should yield ≤ loose ratio ({})",
tight.len(),
loose.len()
);
}
proptest! {
#[test]
fn prop_sparse_fft_k_zero_always_empty(
n_log2 in 4usize..=7usize,
) {
let n = 1_usize << n_log2;
let signal = vec![Complex::new(1.0_f64, 0.0); n];
let result = sparse_fft(&signal, 0);
prop_assert!(result.is_empty(), "k=0 must yield empty result for n={}", n);
}
#[test]
fn prop_sparse_fft_k_one_at_most_one(
n_log2 in 6usize..=8usize,
freq_frac in 0.01f64..=0.99f64,
) {
let n = 1_usize << n_log2;
let freq = ((freq_frac * n as f64) as usize).min(n - 1).max(1);
let two_pi = core::f64::consts::PI * 2.0;
let mut signal = vec![Complex::new(0.0_f64, 0.0); n];
for i in 0..n {
let angle = two_pi * (freq as f64) * (i as f64) / (n as f64);
signal[i] = Complex::new(angle.cos(), angle.sin());
}
let result = sparse_fft(&signal, 1);
prop_assert!(result.len() <= 1, "k=1 should return at most 1, got {}", result.len());
for &idx in &result.indices {
prop_assert!(idx < n, "Index {} ≥ n={}", idx, n);
}
}
#[test]
fn prop_sparse_fft_noise_robust(
n_log2 in 6usize..=7usize,
k in 1usize..=4usize,
noise_seed in 0u64..=10000u64,
) {
let n = 1_usize << n_log2;
let k_clamped = k.min(n / 8).max(1);
let (mut signal, _planted) = build_sparse_signal(n, k_clamped);
let noise_amp = 0.01;
for (i, s) in signal.iter_mut().enumerate() {
let phase = (noise_seed.wrapping_mul(i as u64 + 1)) as f64 * 1e-7;
s.re += noise_amp * phase.sin();
s.im += noise_amp * phase.cos();
}
let result = sparse_fft(&signal, k_clamped);
prop_assert!(
!result.is_empty(),
"Noisy sparse signal (n={}, k={}) should still detect frequencies",
n, k_clamped
);
for &idx in &result.indices {
prop_assert!(idx < n);
}
}
#[test]
fn prop_sparse_fft_auto_detects_something(
n_log2 in 7usize..=8usize,
k in 1usize..=6usize,
) {
let n = 1_usize << n_log2;
let k_clamped = k.min(n / 16).max(1);
let (signal, _planted) = build_sparse_signal(n, k_clamped);
let result = sparse_fft_auto(&signal);
prop_assert!(
!result.is_empty(),
"Auto-detect should find at least 1 frequency for n={}, planted k={}",
n, k_clamped
);
for &idx in &result.indices {
prop_assert!(idx < n, "Index {} ≥ n={}", idx, n);
}
}
#[test]
fn prop_sparse_fft_auto_bounded(
n_log2 in 6usize..=8usize,
) {
let n = 1_usize << n_log2;
let signal = vec![Complex::new(1.0_f64, 0.0); n];
let result = sparse_fft_auto(&signal);
prop_assert!(result.len() <= n, "Auto-detect returned {} > n={}", result.len(), n);
}
#[test]
fn prop_sparse_fft_unique_indices(
n_log2 in 6usize..=8usize,
k in 1usize..=10usize,
) {
let n = 1_usize << n_log2;
let k_clamped = k.min(n / 4).max(1);
let (signal, _) = build_sparse_signal(n, k_clamped);
let result = sparse_fft(&signal, k_clamped);
let mut seen = std::collections::BTreeSet::new();
for &idx in &result.indices {
prop_assert!(
seen.insert(idx),
"Duplicate index {} in sparse_fft result",
idx
);
}
}
}
}