use crate::core::scalar::ControlScalar;
pub struct ThdAnalyzer<S: ControlScalar, const N: usize> {
buffer: [S; N],
head: usize,
count: usize,
pub fundamental_hz: S,
pub sample_rate: S,
}
impl<S: ControlScalar, const N: usize> ThdAnalyzer<S, N> {
pub fn new(fundamental_hz: S, sample_rate: S) -> Self {
Self {
buffer: core::array::from_fn(|_| S::ZERO),
head: 0,
count: 0,
fundamental_hz,
sample_rate,
}
}
pub fn push(&mut self, sample: S) {
self.buffer[self.head] = sample;
self.head = (self.head + 1) % N;
if self.count < N {
self.count += 1;
}
}
pub fn goertzel_magnitude(&self, k: usize) -> S {
let n = self.count;
if n == 0 {
return S::ZERO;
}
let pi = S::PI;
let omega = S::TWO * pi * S::from_f64(k as f64) / S::from_f64(n as f64);
let coeff = S::TWO * omega.cos();
let mut s_prev2 = S::ZERO;
let mut s_prev1 = S::ZERO;
for i in 0..n {
let idx = if self.count < N {
i
} else {
(self.head + i) % N
};
let s = coeff * s_prev1 - s_prev2 + self.buffer[idx];
s_prev2 = s_prev1;
s_prev1 = s;
}
let power = s_prev1 * s_prev1 + s_prev2 * s_prev2 - coeff * s_prev1 * s_prev2;
if power <= S::ZERO {
return S::ZERO;
}
power.sqrt()
}
fn fundamental_bin(&self) -> usize {
if self.count == 0 {
return 1;
}
let bin_f = self.fundamental_hz.to_f64() * self.count as f64 / self.sample_rate.to_f64();
let bin = (bin_f + 0.5) as usize;
bin.max(1).min(self.count / 2)
}
pub fn fundamental_magnitude(&self) -> S {
self.goertzel_magnitude(self.fundamental_bin())
}
pub fn compute_thd(&self) -> S {
let k1 = self.fundamental_bin();
let v1 = self.goertzel_magnitude(k1);
if v1 < S::EPSILON {
return S::ZERO;
}
let max_bin = self.count / 2;
let mut sum_sq = S::ZERO;
let mut h = 2_usize;
loop {
let bin = h * k1;
if bin > max_bin {
break;
}
let vn = self.goertzel_magnitude(bin);
sum_sq += vn * vn;
h += 1;
}
let hundred = S::from_f64(100.0);
sum_sq.sqrt() / v1 * hundred
}
pub fn harmonic_ratio(&self, n: usize) -> S {
let v1 = self.fundamental_magnitude();
if v1 < S::EPSILON {
return S::ZERO;
}
let vn = self.goertzel_magnitude(n);
vn / v1 * S::from_f64(100.0)
}
pub fn sample_count(&self) -> usize {
self.count
}
pub fn clear(&mut self) {
for s in &mut self.buffer {
*s = S::ZERO;
}
self.head = 0;
self.count = 0;
}
}
#[cfg(test)]
mod tests {
use super::*;
fn fill_sine<const N: usize>(
analyzer: &mut ThdAnalyzer<f64, N>,
freq_hz: f64,
sample_rate: f64,
n: usize,
) {
use core::f64::consts::PI;
for i in 0..n {
let t = i as f64 / sample_rate;
let s = (2.0 * PI * freq_hz * t).sin();
analyzer.push(s);
}
}
#[test]
fn pure_sine_has_low_thd() {
let sample_rate = 1000.0_f64;
let f0 = 50.0_f64;
let mut analyzer = ThdAnalyzer::<f64, 64>::new(f0, sample_rate);
fill_sine(&mut analyzer, f0, sample_rate, 64);
let thd = analyzer.compute_thd();
assert!(thd < 20.0, "THD for pure sine too high: {}%", thd);
}
#[test]
fn fundamental_magnitude_nonzero_for_sine() {
let sample_rate = 1000.0_f64;
let f0 = 50.0_f64;
let mut analyzer = ThdAnalyzer::<f64, 64>::new(f0, sample_rate);
fill_sine(&mut analyzer, f0, sample_rate, 64);
let v1 = analyzer.fundamental_magnitude();
assert!(v1 > 0.0, "fundamental magnitude should be > 0, got {}", v1);
}
#[test]
fn zero_signal_gives_zero_thd() {
let mut analyzer = ThdAnalyzer::<f64, 32>::new(50.0, 1000.0);
for _ in 0..32 {
analyzer.push(0.0);
}
assert_eq!(analyzer.compute_thd(), 0.0);
}
#[test]
fn clear_resets_state() {
let mut analyzer = ThdAnalyzer::<f64, 32>::new(50.0, 1000.0);
fill_sine(&mut analyzer, 50.0, 1000.0, 32);
analyzer.clear();
assert_eq!(analyzer.sample_count(), 0);
assert_eq!(analyzer.fundamental_magnitude(), 0.0);
}
}