use super::refinement_window;
pub const DFT_SIZE: usize = 256;
pub const WINDOW_DFT_SIZE: usize = 16384;
pub const W_R_HALF: i32 = 110;
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct Complex64 {
pub re: f64,
pub im: f64,
}
impl Complex64 {
pub const ZERO: Self = Self { re: 0.0, im: 0.0 };
#[inline]
pub const fn new(re: f64, im: f64) -> Self {
Self { re, im }
}
#[inline]
pub fn norm_sqr(self) -> f64 {
self.re * self.re + self.im * self.im
}
}
#[derive(Clone, Debug)]
pub struct HarmonicBasis {
w_r_pos: Box<[f64; WINDOW_DFT_SIZE / 2 + 1]>,
}
impl HarmonicBasis {
pub fn new() -> Self {
let mut table = Box::new([0.0f64; WINDOW_DFT_SIZE / 2 + 1]);
let two_pi = 2.0 * core::f64::consts::PI;
let dft_size = WINDOW_DFT_SIZE as f64;
for (k, slot) in table.iter_mut().enumerate() {
let k_f = k as f64;
let mut acc = 0.0;
acc += f64::from(refinement_window(0));
for n in 1..=W_R_HALF {
let w = f64::from(refinement_window(n));
if w == 0.0 {
continue;
}
let angle = two_pi * k_f * f64::from(n) / dft_size;
acc += 2.0 * w * angle.cos();
}
*slot = acc;
}
Self { w_r_pos: table }
}
#[inline]
pub fn w_r(&self, k: i32) -> f64 {
let abs = k.unsigned_abs() as usize;
if abs >= self.w_r_pos.len() {
0.0
} else {
self.w_r_pos[abs]
}
}
#[inline]
pub fn bin_endpoints(l: u32, omega0: f64) -> (i32, i32) {
let scale = DFT_SIZE as f64 / (2.0 * core::f64::consts::PI);
let a_l = scale * (f64::from(l) - 0.5) * omega0;
let b_l = scale * (f64::from(l) + 0.5) * omega0;
(ceil_i32(a_l), ceil_i32(b_l))
}
pub fn harmonic_amplitude(
&self,
sw: &[Complex64; DFT_SIZE],
l: u32,
omega0: f64,
) -> Complex64 {
let (m_lo, m_hi) = Self::bin_endpoints(l, omega0);
let bin_scale = WINDOW_DFT_SIZE as f64 / (2.0 * core::f64::consts::PI);
let l_offset = bin_scale * f64::from(l) * omega0;
let mut num_re = 0.0;
let mut num_im = 0.0;
let mut den = 0.0;
for m in m_lo..m_hi {
let k = floor_i32(64.0 * f64::from(m) - l_offset + 0.5);
let wr = self.w_r(k);
let s = sw[packed_index(m)];
num_re += s.re * wr;
num_im += s.im * wr;
den += wr * wr;
}
if den > 0.0 {
Complex64::new(num_re / den, num_im / den)
} else {
Complex64::ZERO
}
}
#[inline]
pub fn synthetic_bin(&self, m: i32, l: u32, omega0: f64, a_l: Complex64) -> Complex64 {
let bin_scale = WINDOW_DFT_SIZE as f64 / (2.0 * core::f64::consts::PI);
let k = floor_i32(64.0 * f64::from(m) - bin_scale * f64::from(l) * omega0 + 0.5);
let wr = self.w_r(k);
Complex64::new(a_l.re * wr, a_l.im * wr)
}
}
impl Default for HarmonicBasis {
fn default() -> Self {
Self::new()
}
}
pub fn signal_spectrum(signal_centered: &[f64; (2 * W_R_HALF + 1) as usize]) -> [Complex64; DFT_SIZE] {
use std::sync::OnceLock;
use num_complex::Complex;
use rustfft::{Fft, FftPlanner};
static FFT: OnceLock<std::sync::Arc<dyn Fft<f64>>> = OnceLock::new();
let fft = FFT.get_or_init(|| {
let mut planner = FftPlanner::<f64>::new();
planner.plan_fft_forward(DFT_SIZE)
});
let mut buf = [Complex::<f64>::new(0.0, 0.0); DFT_SIZE];
for n in -W_R_HALF..=W_R_HALF {
let w = f64::from(refinement_window(n));
if w == 0.0 {
continue;
}
let s = signal_centered[(n + W_R_HALF) as usize];
let idx = ((n.rem_euclid(DFT_SIZE as i32)) as usize) % DFT_SIZE;
buf[idx] = Complex::new(s * w, 0.0);
}
fft.process(&mut buf);
let mut out = [Complex64::ZERO; DFT_SIZE];
for (slot, c) in out.iter_mut().zip(buf.iter()) {
*slot = Complex64::new(c.re, c.im);
}
out
}
#[inline]
pub fn packed_index(m: i32) -> usize {
if m < 0 {
(m + DFT_SIZE as i32) as usize
} else {
(m as usize) & (DFT_SIZE - 1)
}
}
#[inline]
pub fn unpacked_m(idx: usize) -> i32 {
if idx <= DFT_SIZE / 2 {
idx as i32
} else {
idx as i32 - DFT_SIZE as i32
}
}
#[inline]
pub(super) fn ceil_i32(x: f64) -> i32 {
x.ceil() as i32
}
#[inline]
pub(super) fn floor_i32(x: f64) -> i32 {
x.floor() as i32
}
#[cfg(test)]
mod tests {
use super::*;
use core::f64::consts::PI;
#[test]
fn bin_endpoints_match_addendum_numerical_example() {
let omega0 = 2.0 * PI / 50.0;
assert_eq!(HarmonicBasis::bin_endpoints(0, omega0), (-2, 3));
assert_eq!(HarmonicBasis::bin_endpoints(1, omega0), (3, 8));
assert_eq!(HarmonicBasis::bin_endpoints(2, omega0), (8, 13));
}
#[test]
fn bin_endpoints_are_contiguous_across_harmonics() {
for omega_denom in [20i32, 50, 100, 123] {
let omega0 = 2.0 * PI / f64::from(omega_denom);
for l in 0..10 {
let (_, b_this) = HarmonicBasis::bin_endpoints(l, omega0);
let (a_next, _) = HarmonicBasis::bin_endpoints(l + 1, omega0);
assert_eq!(
b_this, a_next,
"ω₀ = 2π/{omega_denom}, harmonic {l}: b_l = {b_this}, a_(l+1) = {a_next}"
);
}
}
}
#[test]
fn w_r_zero_equals_window_sum() {
let basis = HarmonicBasis::new();
let expected: f64 = (-W_R_HALF..=W_R_HALF)
.map(|n| f64::from(refinement_window(n)))
.sum();
assert!(
(basis.w_r(0) - expected).abs() < 1e-9,
"W_R(0) = {}, expected {}",
basis.w_r(0),
expected
);
}
#[test]
fn w_r_is_symmetric_in_k() {
let basis = HarmonicBasis::new();
for k in [1, 7, 64, 100, 500, 1023, 8000] {
assert_eq!(basis.w_r(k), basis.w_r(-k), "W_R({k}) != W_R(−{k})");
}
}
#[test]
fn w_r_returns_zero_out_of_range() {
let basis = HarmonicBasis::new();
assert_eq!(basis.w_r(WINDOW_DFT_SIZE as i32), 0.0);
assert_eq!(basis.w_r(-(WINDOW_DFT_SIZE as i32)), 0.0);
}
#[test]
fn packed_index_roundtrips_two_sided_range() {
for m in -127..=128 {
let idx = packed_index(m);
assert!(idx < DFT_SIZE, "packed_index({m}) out of bounds");
assert_eq!(unpacked_m(idx), m, "roundtrip failed for m = {m}");
}
}
fn signal_spectrum_direct_reference(
signal_centered: &[f64; (2 * W_R_HALF + 1) as usize],
) -> [Complex64; DFT_SIZE] {
let mut out = [Complex64::ZERO; DFT_SIZE];
let two_pi = 2.0 * core::f64::consts::PI;
let dft_size = DFT_SIZE as f64;
for (idx, slot) in out.iter_mut().enumerate() {
let m = unpacked_m(idx);
let m_f = f64::from(m);
let mut re = 0.0;
let mut im = 0.0;
for n in -W_R_HALF..=W_R_HALF {
let w = f64::from(refinement_window(n));
if w == 0.0 {
continue;
}
let s = signal_centered[(n + W_R_HALF) as usize];
let angle = -two_pi * m_f * f64::from(n) / dft_size;
re += s * w * angle.cos();
im += s * w * angle.sin();
}
*slot = Complex64::new(re, im);
}
out
}
#[test]
fn signal_spectrum_fft_matches_direct_reference() {
let mut signal = [0.0f64; (2 * W_R_HALF + 1) as usize];
let two_pi = 2.0 * core::f64::consts::PI;
for (i, slot) in signal.iter_mut().enumerate() {
let n = i as f64 - W_R_HALF as f64;
*slot = 100.0 + 1000.0 * (two_pi * n / 50.0).sin()
+ 500.0 * (two_pi * n / 23.5).cos();
}
let fft = signal_spectrum(&signal);
let dir = signal_spectrum_direct_reference(&signal);
for (k, (a, b)) in fft.iter().zip(dir.iter()).enumerate() {
let de = (a.re - b.re).abs();
let di = (a.im - b.im).abs();
let scale = a.re.abs().max(b.re.abs()).max(1.0);
assert!(
de / scale < 1e-9 && di / scale < 1e-9,
"bin {k} mismatch: FFT={a:?}, direct={b:?}, |Δre|={de}, |Δim|={di}"
);
}
}
#[test]
fn signal_spectrum_of_zero_is_zero() {
let signal = [0.0f64; (2 * W_R_HALF + 1) as usize];
let sw = signal_spectrum(&signal);
for (i, s) in sw.iter().enumerate() {
assert!(s.norm_sqr() < 1e-18, "S_w[{i}] = ({}, {})", s.re, s.im);
}
}
#[test]
fn signal_spectrum_of_constant_matches_w_r_at_dc() {
let signal = [1.0f64; (2 * W_R_HALF + 1) as usize];
let sw = signal_spectrum(&signal);
let basis = HarmonicBasis::new();
let s0 = sw[packed_index(0)];
assert!(
(s0.re - basis.w_r(0)).abs() < 1e-9 && s0.im.abs() < 1e-9,
"S_w(0) = ({}, {}), expected ({}, 0)",
s0.re,
s0.im,
basis.w_r(0)
);
}
fn cosine_tone(omega0: f64) -> [f64; (2 * W_R_HALF + 1) as usize] {
let mut signal = [0.0f64; (2 * W_R_HALF + 1) as usize];
for n in -W_R_HALF..=W_R_HALF {
signal[(n + W_R_HALF) as usize] = (omega0 * f64::from(n)).cos();
}
signal
}
fn sine_tone(omega0: f64) -> [f64; (2 * W_R_HALF + 1) as usize] {
let mut signal = [0.0f64; (2 * W_R_HALF + 1) as usize];
for n in -W_R_HALF..=W_R_HALF {
signal[(n + W_R_HALF) as usize] = (omega0 * f64::from(n)).sin();
}
signal
}
#[test]
fn harmonic_projection_recovers_cosine_amplitude() {
let basis = HarmonicBasis::new();
let omega0 = 2.0 * PI / 50.0;
let signal = cosine_tone(omega0);
let sw = signal_spectrum(&signal);
let a1 = basis.harmonic_amplitude(&sw, 1, omega0);
assert!(
(a1.re - 0.5).abs() < 0.01 && a1.im.abs() < 0.01,
"A_1 = ({}, {}), expected ≈ (0.5, 0)",
a1.re,
a1.im
);
}
#[test]
fn harmonic_projection_recovers_sine_amplitude() {
let basis = HarmonicBasis::new();
let omega0 = 2.0 * PI / 50.0;
let signal = sine_tone(omega0);
let sw = signal_spectrum(&signal);
let a1 = basis.harmonic_amplitude(&sw, 1, omega0);
assert!(
a1.re.abs() < 0.01 && (a1.im + 0.5).abs() < 0.01,
"A_1 = ({}, {}), expected ≈ (0, −0.5)",
a1.re,
a1.im
);
}
#[test]
fn harmonic_projection_isolates_second_harmonic() {
let basis = HarmonicBasis::new();
let omega0 = 2.0 * PI / 50.0;
let mut signal = [0.0f64; (2 * W_R_HALF + 1) as usize];
for n in -W_R_HALF..=W_R_HALF {
signal[(n + W_R_HALF) as usize] = (2.0 * omega0 * f64::from(n)).cos();
}
let sw = signal_spectrum(&signal);
let a1 = basis.harmonic_amplitude(&sw, 1, omega0);
let a2 = basis.harmonic_amplitude(&sw, 2, omega0);
assert!(
a1.re.abs() < 0.05 && a1.im.abs() < 0.05,
"A_1 leak = ({}, {})",
a1.re,
a1.im
);
assert!(
(a2.re - 0.5).abs() < 0.01 && a2.im.abs() < 0.01,
"A_2 = ({}, {}), expected ≈ (0.5, 0)",
a2.re,
a2.im
);
}
#[test]
fn synthetic_bin_reconstructs_clean_sinusoid() {
let basis = HarmonicBasis::new();
let omega0 = 2.0 * PI / 50.0;
let signal = cosine_tone(omega0);
let sw = signal_spectrum(&signal);
let a1 = basis.harmonic_amplitude(&sw, 1, omega0);
let (m_lo, m_hi) = HarmonicBasis::bin_endpoints(1, omega0);
let mut residual_energy = 0.0;
let mut signal_energy = 0.0;
for m in m_lo..m_hi {
let observed = sw[packed_index(m)];
let synthetic = basis.synthetic_bin(m, 1, omega0, a1);
let dre = observed.re - synthetic.re;
let dim = observed.im - synthetic.im;
residual_energy += dre * dre + dim * dim;
signal_energy += observed.norm_sqr();
}
assert!(
residual_energy / signal_energy.max(1e-12) < 0.01,
"residual/signal = {}, expected < 0.01",
residual_energy / signal_energy.max(1e-12)
);
}
#[test]
fn harmonic_amplitude_empty_support_returns_zero() {
let basis = HarmonicBasis::new();
let sw = [Complex64::new(1.0, 1.0); DFT_SIZE];
let a0 = basis.harmonic_amplitude(&sw, 0, 0.0);
assert_eq!(a0, Complex64::ZERO);
}
}