use alloc::vec::Vec;
use core::f64::consts::PI;
use alice_det_math::{atan2, cos64, sin64};
#[cfg(not(feature = "std"))]
#[allow(unused_imports)]
use crate::math::FloatExt;
const MIN_MAGNITUDE: f32 = 1e-10;
#[must_use]
pub fn analyze_signal(
values: &[f32],
max_coefficients: usize,
energy_threshold: f32,
) -> (Vec<(usize, f32, f32)>, f32) {
if values.is_empty() || max_coefficients == 0 {
return (Vec::new(), 0.0);
}
let n = values.len();
let dc_offset = mean(values);
let centered: Vec<f32> = values.iter().map(|v| v - dc_offset).collect();
let spectrum: Vec<(usize, f32, f32)> = (1..=n / 2)
.map(|k| {
let (re, im) = dft_bin(¢ered, k);
(k, (re * re + im * im).sqrt(), atan2(im, re))
})
.collect();
(
select_coefficients(spectrum, max_coefficients, energy_threshold),
dc_offset,
)
}
#[cfg(feature = "fft")]
#[must_use]
pub fn analyze_signal_fft(
values: &[f32],
max_coefficients: usize,
energy_threshold: f32,
) -> (Vec<(usize, f32, f32)>, f32) {
use rustfft::num_complex::Complex;
use rustfft::FftPlanner;
if values.is_empty() || max_coefficients == 0 {
return (Vec::new(), 0.0);
}
let n = values.len();
let dc_offset = mean(values);
let mut buffer: Vec<Complex<f32>> = values
.iter()
.map(|&x| Complex::new(x - dc_offset, 0.0))
.collect();
FftPlanner::new().plan_fft_forward(n).process(&mut buffer);
let spectrum: Vec<(usize, f32, f32)> = buffer[1..=n / 2]
.iter()
.enumerate()
.map(|(i, c)| (i + 1, c.norm(), c.arg()))
.collect();
(
select_coefficients(spectrum, max_coefficients, energy_threshold),
dc_offset,
)
}
#[must_use]
#[allow(clippy::cast_precision_loss)]
pub fn fourier_at(n: usize, coefficients: &[(usize, f32, f32)], dc_offset: f32, i: f64) -> f32 {
if n == 0 {
return dc_offset;
}
let inv_n = 1.0 / n as f64;
let mut sum = f64::from(dc_offset);
for &(k, mag, phase) in coefficients {
if k >= n {
continue;
}
let weight = if k == 0 || 2 * k == n { 1.0 } else { 2.0 };
let theta = 2.0 * PI * k as f64 * i * inv_n + f64::from(phase);
sum += weight * f64::from(mag) * inv_n * cos64(theta);
}
sum as f32
}
#[must_use]
#[allow(clippy::cast_precision_loss)]
pub fn sine_at(
n: usize,
frequency: f32,
amplitude: f32,
phase: f32,
dc_offset: f32,
i: f64,
) -> f32 {
let inv_n = if n == 0 { 0.0 } else { 1.0 / n as f64 };
let theta = 2.0 * PI * f64::from(frequency) * i * inv_n + f64::from(phase);
(f64::from(dc_offset) + f64::from(amplitude) * sin64(theta)) as f32
}
#[must_use]
#[allow(clippy::cast_precision_loss)]
pub fn multi_sine_at(n: usize, components: &[(f32, f32, f32)], dc_offset: f32, i: f64) -> f32 {
let inv_n = if n == 0 { 0.0 } else { 1.0 / n as f64 };
let mut sum = f64::from(dc_offset);
for &(freq, amp, phase) in components {
let theta = 2.0 * PI * f64::from(freq) * i * inv_n + f64::from(phase);
sum += f64::from(amp) * sin64(theta);
}
sum as f32
}
#[must_use]
#[allow(clippy::cast_precision_loss)]
pub fn generate_from_coefficients(
n: usize,
coefficients: &[(usize, f32, f32)],
dc_offset: f32,
) -> Vec<f32> {
(0..n)
.map(|i| fourier_at(n, coefficients, dc_offset, i as f64))
.collect()
}
#[must_use]
#[allow(clippy::cast_precision_loss)]
pub fn generate_sine_wave(
n: usize,
frequency: f32,
amplitude: f32,
phase: f32,
dc_offset: f32,
) -> Vec<f32> {
(0..n)
.map(|i| sine_at(n, frequency, amplitude, phase, dc_offset, i as f64))
.collect()
}
#[must_use]
#[allow(clippy::cast_precision_loss)]
pub fn generate_multi_sine(n: usize, components: &[(f32, f32, f32)], dc_offset: f32) -> Vec<f32> {
(0..n)
.map(|i| multi_sine_at(n, components, dc_offset, i as f64))
.collect()
}
#[allow(clippy::cast_precision_loss)]
fn mean(values: &[f32]) -> f32 {
values.iter().sum::<f32>() / values.len() as f32
}
fn select_coefficients(
mut spectrum: Vec<(usize, f32, f32)>,
max_coefficients: usize,
energy_threshold: f32,
) -> Vec<(usize, f32, f32)> {
spectrum.retain(|&(_, mag, _)| mag >= MIN_MAGNITUDE);
let total_energy: f32 = spectrum.iter().map(|&(_, m, _)| m * m).sum();
spectrum.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(core::cmp::Ordering::Equal));
spectrum.truncate(max_coefficients);
if total_energy > 0.0 && energy_threshold > 0.0 && energy_threshold < 1.0 {
let cutoff = total_energy * energy_threshold;
let mut cum = 0.0_f32;
let mut keep = 0;
for (i, &(_, m, _)) in spectrum.iter().enumerate() {
cum += m * m;
keep = i + 1;
if cum >= cutoff {
break;
}
}
spectrum.truncate(keep);
}
spectrum
}
#[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
fn dft_bin(values: &[f32], k: usize) -> (f32, f32) {
let omega = 2.0 * core::f64::consts::PI * k as f64 / values.len() as f64;
let mut re = 0.0_f64;
let mut im = 0.0_f64;
for (i, &v) in values.iter().enumerate() {
let theta = omega * i as f64;
re += f64::from(v) * cos64(theta);
im -= f64::from(v) * sin64(theta);
}
(re as f32, im as f32)
}
#[cfg(test)]
#[allow(clippy::cast_precision_loss)]
mod tests {
use super::*;
use alloc::vec;
#[test]
fn generate_sine_wave_shape() {
let out = generate_sine_wave(4, 1.0, 1.0, 0.0, 0.0);
assert!(out[0].abs() < 1e-5);
assert!((out[1] - 1.0).abs() < 1e-5);
assert!(out[2].abs() < 1e-5);
assert!((out[3] + 1.0).abs() < 1e-5);
assert!(generate_sine_wave(0, 1.0, 1.0, 0.0, 0.0).is_empty());
assert!(generate_multi_sine(0, &[(1.0, 1.0, 0.0)], 0.0).is_empty());
}
#[test]
fn analyze_signal_pure_tone() {
let n = 32usize;
let samples = generate_sine_wave(n, 1.0, 2.0, 0.0, 0.0);
let (coefs, dc) = analyze_signal(&samples, 4, 0.99);
assert!(dc.abs() < 1e-4, "DC offset should be ~0");
assert_eq!(coefs.len(), 1, "one tone → one bin: {coefs:?}");
assert_eq!(coefs[0].0, 1);
assert!((coefs[0].1 - 32.0).abs() < 1e-3, "{}", coefs[0].1);
}
#[test]
fn round_trip_pure_tone() {
let n = 32usize;
let samples = generate_sine_wave(n, 1.0, 2.0, 0.0, 3.0);
let (coefs, dc) = analyze_signal(&samples, 4, 0.99);
let recon = generate_from_coefficients(n, &coefs, dc);
assert_eq!(recon.len(), n);
for (a, b) in samples.iter().zip(&recon) {
assert!((a - b).abs() < 1e-4, "{a} vs {b}");
}
}
#[test]
fn analyze_signal_empty_and_zero_max() {
let (c, dc) = analyze_signal(&[], 3, 0.9);
assert!(c.is_empty() && dc == 0.0);
let (c, _) = analyze_signal(&[1.0, 2.0, 3.0], 0, 0.9);
assert!(c.is_empty());
let (c, dc) = analyze_signal(&[5.0; 16], 3, 0.9);
assert!(c.is_empty());
assert!((dc - 5.0).abs() < 1e-6);
}
#[test]
fn generate_from_coefficients_edge_cases() {
assert!(generate_from_coefficients(0, &[], 0.0).is_empty());
let out = generate_from_coefficients(4, &[(4, 100.0, 0.0), (9, 1.0, 0.0)], 2.0);
assert_eq!(out, vec![2.0; 4]);
}
#[test]
fn energy_threshold_uses_total_energy() {
let n = 64usize;
let s = generate_multi_sine(n, &[(3.0, 4.0, 0.0), (7.0, 3.0, 0.0)], 0.0);
let (c, _) = analyze_signal(&s, 8, 0.9);
assert_eq!(c.len(), 2, "{c:?}");
let (c, _) = analyze_signal(&s, 8, 0.5);
assert_eq!(c.len(), 1, "{c:?}");
assert_eq!(c[0].0, 3);
}
#[cfg(feature = "fft")]
#[test]
fn fft_matches_naive_dft() {
let n = 128usize;
let s = generate_multi_sine(n, &[(3.0, 4.0, 0.3), (17.0, 1.5, -1.0)], 0.7);
let (a, dc_a) = analyze_signal(&s, 2, 1.0);
let (b, dc_b) = analyze_signal_fft(&s, 2, 1.0);
assert!((dc_a - dc_b).abs() < 1e-5);
assert_eq!(a.len(), 2);
assert_eq!(a.len(), b.len());
for ((ka, ma, pa), (kb, mb, pb)) in a.iter().zip(&b) {
assert_eq!(ka, kb);
assert!((ma - mb).abs() < 1e-2 * ma.max(1.0), "{ma} vs {mb}");
assert!((pa - pb).abs() < 1e-3, "{pa} vs {pb}");
}
let (a, dc_a) = analyze_signal(&s, n / 2, 1.0);
let (b, dc_b) = analyze_signal_fft(&s, n / 2, 1.0);
let ra = generate_from_coefficients(n, &a, dc_a);
let rb = generate_from_coefficients(n, &b, dc_b);
for ((x, y), orig) in ra.iter().zip(&rb).zip(&s) {
assert!((x - y).abs() < 1e-4, "{x} vs {y}");
assert!((x - orig).abs() < 1e-3, "{x} vs {orig}");
}
}
}