#![allow(clippy::needless_range_loop)]
use crate::core::scalar::ControlScalar;
use heapless::Vec;
#[derive(Debug, Clone)]
pub struct HarmonicDetector<S: ControlScalar, const N: usize> {
pub orders: [u32; N],
window_len: usize,
samples: Vec<S, 2048>,
thetas: Vec<S, 2048>,
coeff_a: [S; N],
coeff_b: [S; N],
}
impl<S: ControlScalar, const N: usize> HarmonicDetector<S, N> {
pub fn new(orders: [u32; N], window_len: usize) -> Self {
Self {
orders,
window_len,
samples: Vec::new(),
thetas: Vec::new(),
coeff_a: [S::ZERO; N],
coeff_b: [S::ZERO; N],
}
}
pub fn update(&mut self, i_load: S, theta_grid: S) -> [S; N] {
let _ = self.samples.push(i_load);
let _ = self.thetas.push(theta_grid);
if self.samples.len() >= self.window_len {
self.compute_dft();
self.samples.clear();
self.thetas.clear();
}
self.coeff_a
}
fn compute_dft(&mut self) {
let len = self.samples.len();
if len == 0 {
return;
}
let inv_len = S::ONE / S::from_f64(len as f64);
let two = S::TWO;
for k in 0..N {
let h = S::from_f64(self.orders[k] as f64);
let mut a = S::ZERO;
let mut b = S::ZERO;
for j in 0..len {
let theta = self.thetas[j];
let x = self.samples[j];
a += x * (h * theta).cos();
b += x * (h * theta).sin();
}
self.coeff_a[k] = two * a * inv_len;
self.coeff_b[k] = two * b * inv_len;
}
}
pub fn amplitude_cos(&self, k: usize) -> S {
if k < N {
self.coeff_a[k]
} else {
S::ZERO
}
}
pub fn amplitude_sin(&self, k: usize) -> S {
if k < N {
self.coeff_b[k]
} else {
S::ZERO
}
}
pub fn amplitude_peak(&self, k: usize) -> S {
if k < N {
(self.coeff_a[k] * self.coeff_a[k] + self.coeff_b[k] * self.coeff_b[k]).sqrt()
} else {
S::ZERO
}
}
pub fn reset(&mut self) {
self.samples.clear();
self.thetas.clear();
self.coeff_a = [S::ZERO; N];
self.coeff_b = [S::ZERO; N];
}
}
#[derive(Debug, Clone)]
pub struct ApfCurrentReference<S: ControlScalar, const N: usize> {
pub detector: HarmonicDetector<S, N>,
coeff_a: [S; N],
coeff_b: [S; N],
}
impl<S: ControlScalar, const N: usize> ApfCurrentReference<S, N> {
pub fn new(orders: [u32; N], window_len: usize) -> Self {
Self {
detector: HarmonicDetector::new(orders, window_len),
coeff_a: [S::ZERO; N],
coeff_b: [S::ZERO; N],
}
}
pub fn compute_reference(&mut self, i_load: S, theta: S) -> S {
let new_a = self.detector.update(i_load, theta);
for k in 0..N {
self.coeff_a[k] = new_a[k];
self.coeff_b[k] = self.detector.amplitude_sin(k);
}
let mut i_ref = S::ZERO;
for k in 0..N {
let h = S::from_f64(self.detector.orders[k] as f64);
i_ref -= self.coeff_a[k] * (h * theta).cos() + self.coeff_b[k] * (h * theta).sin();
}
i_ref
}
pub fn reset(&mut self) {
self.detector.reset();
self.coeff_a = [S::ZERO; N];
self.coeff_b = [S::ZERO; N];
}
}
#[derive(Debug, Clone, Copy)]
pub struct ApfController<S: ControlScalar> {
pub h_band: S,
state: bool,
}
impl<S: ControlScalar> ApfController<S> {
pub fn new(h_band: S) -> Self {
Self {
h_band,
state: false,
}
}
pub fn update(&mut self, i_ref: S, i_actual: S) -> bool {
let err = i_ref - i_actual;
if err > self.h_band {
self.state = true;
} else if err < -self.h_band {
self.state = false;
}
self.state
}
pub fn switching_state(&self) -> bool {
self.state
}
pub fn reset(&mut self) {
self.state = false;
}
}
#[cfg(test)]
mod tests {
use super::*;
use core::f64::consts::PI;
const OMEGA: f64 = 2.0 * PI * 50.0;
fn load_current(t: f64, i1: f64, i5: f64, i7: f64) -> f64 {
i1 * (OMEGA * t).sin() + i5 * (5.0 * OMEGA * t).sin() + i7 * (7.0 * OMEGA * t).sin()
}
#[test]
fn fifth_harmonic_detection_accuracy() {
let fs = 10_000.0_f64; let dt = 1.0 / fs;
let window_len = (fs / 50.0).round() as usize;
let mut det: HarmonicDetector<f64, 2> = HarmonicDetector::new([5, 7], window_len);
let i5_true = 2.0_f64;
let i7_true = 1.0_f64;
let n_samples = 3 * window_len;
for k in 0..n_samples {
let t = k as f64 * dt;
let theta = OMEGA * t;
let i_load = load_current(t, 10.0, i5_true, i7_true);
det.update(i_load, theta);
}
let a5 = det.amplitude_peak(0);
let a7 = det.amplitude_peak(1);
assert!(
(a5 - i5_true).abs() < 0.2,
"5th harmonic: detected={a5:.4}, true={i5_true:.4}"
);
assert!(
(a7 - i7_true).abs() < 0.15,
"7th harmonic: detected={a7:.4}, true={i7_true:.4}"
);
}
#[test]
fn zero_fundamental_in_filter_output() {
let fs = 10_000.0_f64;
let dt = 1.0 / fs;
let window_len = (fs / 50.0).round() as usize;
let mut apf: ApfCurrentReference<f64, 2> = ApfCurrentReference::new([5, 7], window_len);
let i1 = 10.0_f64;
let i5 = 3.0_f64;
let i7 = 1.5_f64;
let n_prime = 3 * window_len;
for k in 0..n_prime {
let t = k as f64 * dt;
let theta = OMEGA * t;
let i_load = load_current(t, i1, i5, i7);
apf.compute_reference(i_load, theta);
}
let mut fund_cos_acc = 0.0_f64;
let mut fund_sin_acc = 0.0_f64;
let mut count = 0usize;
for k in 0..window_len {
let t = (n_prime + k) as f64 * dt;
let theta = OMEGA * t;
let i_load = load_current(t, i1, i5, i7);
let i_ref = apf.compute_reference(i_load, theta);
fund_cos_acc += i_ref * theta.cos();
fund_sin_acc += i_ref * theta.sin();
count += 1;
}
let fund_cos = 2.0 * fund_cos_acc / count as f64;
let fund_sin = 2.0 * fund_sin_acc / count as f64;
let fund_amp = (fund_cos * fund_cos + fund_sin * fund_sin).sqrt();
let threshold = 0.05 * i1;
assert!(
fund_amp < threshold,
"Fundamental leakage in APF ref = {fund_amp:.4} A (threshold={threshold:.4})"
);
}
#[test]
fn hysteresis_switches_high_on_positive_error() {
let mut ctrl = ApfController::new(0.5_f64);
let sw = ctrl.update(5.0, 4.0);
assert!(sw, "Should switch HIGH (error > band)");
}
#[test]
fn hysteresis_switches_low_on_negative_error() {
let mut ctrl = ApfController::new(0.5_f64);
ctrl.update(5.0, 4.0);
let sw = ctrl.update(5.0, 6.0);
assert!(!sw, "Should switch LOW (error < -band)");
}
#[test]
fn hysteresis_holds_state_within_band() {
let mut ctrl = ApfController::new(1.0_f64);
ctrl.update(5.0, 3.0);
let sw = ctrl.update(5.0, 4.5);
assert!(sw, "Should hold HIGH within band");
}
#[test]
fn detector_reset_clears_state() {
let mut det: HarmonicDetector<f64, 1> = HarmonicDetector::new([5], 200);
for k in 0..100 {
det.update(k as f64 * 0.1, 0.01 * k as f64);
}
det.reset();
assert_eq!(det.amplitude_cos(0), 0.0);
assert_eq!(det.amplitude_sin(0), 0.0);
}
#[test]
fn apf_controller_reset() {
let mut ctrl = ApfController::new(0.5_f64);
ctrl.update(5.0, 3.0); ctrl.reset();
assert!(!ctrl.switching_state(), "Should be LOW after reset");
}
}