use crate::error::{OxiGridError, Result};
use serde::{Deserialize, Serialize};
use std::f64::consts::PI;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HarmonicComponent {
pub order: usize,
pub magnitude_pu: f64,
pub phase_rad: f64,
pub power: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WaveformMetrics {
pub thd_pct: f64,
pub crest_factor: f64,
pub form_factor: f64,
pub k_factor: f64,
pub distortion_power: f64,
pub displacement_pf: f64,
pub true_pf: f64,
pub harmonic_components: Vec<HarmonicComponent>,
}
pub fn goertzel(samples: &[f64], freq_hz: f64, sample_rate_hz: f64) -> (f64, f64) {
let n = samples.len();
if n == 0 || sample_rate_hz <= 0.0 {
return (0.0, 0.0);
}
let k = (freq_hz / sample_rate_hz * n as f64).round() as usize;
let omega = 2.0 * PI * k as f64 / n as f64;
let coeff = 2.0 * omega.cos();
let mut s1 = 0.0_f64;
let mut s2 = 0.0_f64;
for &x in samples {
let s = x + coeff * s1 - s2;
s2 = s1;
s1 = s;
}
let re = s1 - s2 * omega.cos();
let im = s2 * omega.sin();
(re / n as f64 * 2.0, im / n as f64 * 2.0)
}
pub fn extract_harmonics(
waveform: &[f64],
sample_rate_hz: f64,
fundamental_hz: f64,
n_harmonics: usize,
) -> Vec<HarmonicComponent> {
if waveform.is_empty() || n_harmonics == 0 || sample_rate_hz <= 0.0 || fundamental_hz <= 0.0 {
return vec![];
}
let nyquist = sample_rate_hz / 2.0;
let (re1, im1) = goertzel(waveform, fundamental_hz, sample_rate_hz);
let peak1 = (re1 * re1 + im1 * im1).sqrt();
let phase1 = im1.atan2(re1);
let mut components = Vec::with_capacity(n_harmonics);
for h in 1..=n_harmonics {
let freq = fundamental_hz * h as f64;
if freq > nyquist {
break;
}
let (re, im) = goertzel(waveform, freq, sample_rate_hz);
let peak = (re * re + im * im).sqrt();
let magnitude_pu = peak / 2_f64.sqrt();
let phase_abs = im.atan2(re);
let phase_rel = if h == 1 { 0.0 } else { phase_abs - phase1 };
components.push(HarmonicComponent {
order: h,
magnitude_pu,
phase_rad: phase_rel,
power: 0.0,
});
let _ = peak1; }
components
}
pub fn compute_k_factor(harmonics: &[HarmonicComponent]) -> f64 {
let numerator: f64 = harmonics
.iter()
.map(|h| (h.order * h.order) as f64 * h.magnitude_pu * h.magnitude_pu)
.sum();
let denominator: f64 = harmonics
.iter()
.map(|h| h.magnitude_pu * h.magnitude_pu)
.sum();
if denominator < 1e-15 {
1.0
} else {
numerator / denominator
}
}
pub fn transformer_derating_factor(k_factor: f64, k_rated: f64) -> f64 {
let k_rated_safe = k_rated.max(1.0);
let k_factor_safe = k_factor.max(1.0);
let eddy_loss_ratio = 0.10_f64;
let denom = 1.0 + eddy_loss_ratio * k_factor_safe / k_rated_safe;
(1.0 / denom).sqrt()
}
pub fn detect_interharmonics(
spectrum: &[f64],
sample_rate_hz: f64,
fundamental_hz: f64,
threshold: f64,
) -> Vec<(f64, f64)> {
let n = spectrum.len();
if n == 0 || sample_rate_hz <= 0.0 || fundamental_hz <= 0.0 {
return vec![];
}
let freq_resolution = sample_rate_hz / n as f64;
let half_window = (freq_resolution * 0.5).max(1.0);
let mut result = Vec::new();
for (k, &mag) in spectrum.iter().enumerate() {
if mag < threshold {
continue;
}
let freq = k as f64 * freq_resolution;
let nearest_order = (freq / fundamental_hz).round();
let harmonic_freq = nearest_order * fundamental_hz;
if (freq - harmonic_freq).abs() > half_window {
result.push((freq, mag));
}
}
result.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
result
}
pub fn analyze_waveform(
voltage: &[f64],
current: &[f64],
sample_rate_hz: f64,
nominal_freq_hz: f64,
n_harmonics: usize,
) -> Result<WaveformMetrics> {
let n = voltage.len();
if n == 0 {
return Err(OxiGridError::InvalidParameter(
"voltage waveform is empty".to_string(),
));
}
if current.len() != n {
return Err(OxiGridError::InvalidParameter(format!(
"voltage length {} ≠ current length {}",
n,
current.len()
)));
}
if sample_rate_hz <= 0.0 || nominal_freq_hz <= 0.0 {
return Err(OxiGridError::InvalidParameter(
"sample_rate_hz and nominal_freq_hz must be positive".to_string(),
));
}
if n_harmonics == 0 {
return Err(OxiGridError::InvalidParameter(
"n_harmonics must be ≥ 1".to_string(),
));
}
let v_rms = rms(voltage);
let v_peak = voltage
.iter()
.copied()
.fold(f64::NEG_INFINITY, f64::max)
.abs()
.max(voltage.iter().copied().fold(f64::INFINITY, f64::min).abs());
let v_avg_rect = voltage.iter().map(|&v| v.abs()).sum::<f64>() / n as f64;
let crest_factor = if v_rms > 1e-15 { v_peak / v_rms } else { 0.0 };
let form_factor = if v_avg_rect > 1e-15 {
v_rms / v_avg_rect
} else {
0.0
};
let v_harmonics = extract_harmonics(voltage, sample_rate_hz, nominal_freq_hz, n_harmonics);
let i_harmonics = extract_harmonics(current, sample_rate_hz, nominal_freq_hz, n_harmonics);
let v1 = v_harmonics
.first()
.map(|h| h.magnitude_pu)
.unwrap_or(0.0)
.max(1e-15);
let i1 = i_harmonics.first().map(|h| h.magnitude_pu).unwrap_or(0.0);
let thd_v_sq: f64 = v_harmonics
.iter()
.skip(1)
.map(|h| h.magnitude_pu.powi(2))
.sum();
let thd_pct = thd_v_sq.sqrt() / v1 * 100.0;
let k_factor = compute_k_factor(&i_harmonics);
let active_power: f64 = voltage
.iter()
.zip(current.iter())
.map(|(&v, &i)| v * i)
.sum::<f64>()
/ n as f64;
let i_rms = rms(current);
let apparent_power = v_rms * i_rms;
let phi1_v = v_harmonics.first().map(|h| h.phase_rad).unwrap_or(0.0);
let phi1_i = i_harmonics.first().map(|h| h.phase_rad).unwrap_or(0.0);
let phi1 = phi1_v - phi1_i;
let q1 = v1 * i1 * phi1.sin();
let s2 = apparent_power * apparent_power;
let distortion_power_sq = (s2 - active_power * active_power - q1 * q1).max(0.0);
let distortion_power = distortion_power_sq.sqrt();
let displacement_pf = phi1.cos();
let true_pf = if apparent_power > 1e-15 {
(active_power / apparent_power).clamp(-1.0, 1.0)
} else {
1.0
};
let harmonic_components: Vec<HarmonicComponent> = v_harmonics
.iter()
.enumerate()
.map(|(idx, vh)| {
let ih = i_harmonics.get(idx);
let hp = ih
.map(|i| {
let dphi = vh.phase_rad - i.phase_rad;
vh.magnitude_pu * i.magnitude_pu * dphi.cos()
})
.unwrap_or(0.0);
HarmonicComponent {
order: vh.order,
magnitude_pu: vh.magnitude_pu,
phase_rad: vh.phase_rad,
power: hp,
}
})
.collect();
Ok(WaveformMetrics {
thd_pct,
crest_factor,
form_factor,
k_factor,
distortion_power,
displacement_pf,
true_pf,
harmonic_components,
})
}
fn rms(samples: &[f64]) -> f64 {
if samples.is_empty() {
return 0.0;
}
let mean_sq = samples.iter().map(|&v| v * v).sum::<f64>() / samples.len() as f64;
mean_sq.sqrt()
}
#[cfg(test)]
mod tests {
use super::*;
fn sine(amp: f64, freq_hz: f64, phase_rad: f64, fs: f64, n: usize) -> Vec<f64> {
(0..n)
.map(|i| amp * (2.0 * PI * freq_hz * i as f64 / fs + phase_rad).sin())
.collect()
}
#[test]
fn test_thd_pure_fundamental() {
let fs = 10_000.0_f64;
let n = (fs / 50.0 * 10.0) as usize; let v = sine(1.0, 50.0, 0.0, fs, n);
let i = v.clone();
let m = analyze_waveform(&v, &i, fs, 50.0, 40).expect("analysis failed");
assert!(
m.thd_pct < 1.0,
"THD for pure sine should be < 1%, got {:.4}",
m.thd_pct
);
}
#[test]
fn test_thd_with_10pct_3rd_harmonic() {
let fs = 10_000.0_f64;
let f0 = 50.0_f64;
let n = (fs / f0 * 10.0) as usize;
let v: Vec<f64> = (0..n)
.map(|i| {
let t = i as f64 / fs;
(2.0 * PI * f0 * t).sin() + 0.1 * (2.0 * PI * 3.0 * f0 * t).sin()
})
.collect();
let i = v.clone();
let m = analyze_waveform(&v, &i, fs, f0, 10).expect("analysis failed");
assert!(
(m.thd_pct - 10.0).abs() < 1.0,
"Expected THD ≈ 10%, got {:.3}%",
m.thd_pct
);
}
#[test]
fn test_crest_factor_sinusoid() {
let fs = 10_000.0_f64;
let f0 = 50.0_f64;
let n = (fs / f0 * 20.0) as usize;
let v = sine(1.0, f0, 0.0, fs, n);
let i = v.clone();
let m = analyze_waveform(&v, &i, fs, f0, 1).expect("analysis failed");
assert!(
(m.crest_factor - 2.0_f64.sqrt()).abs() < 0.05,
"Crest factor should be √2, got {:.4}",
m.crest_factor
);
}
#[test]
fn test_k_factor_fundamental_only() {
let harmonics = vec![HarmonicComponent {
order: 1,
magnitude_pu: 1.0,
phase_rad: 0.0,
power: 0.0,
}];
let k = compute_k_factor(&harmonics);
assert!(
(k - 1.0).abs() < 1e-10,
"K-factor for fundamental-only should be 1.0, got {k:.6}"
);
}
#[test]
fn test_k_factor_with_harmonics() {
let harmonics = vec![
HarmonicComponent {
order: 1,
magnitude_pu: 1.0,
phase_rad: 0.0,
power: 0.0,
},
HarmonicComponent {
order: 3,
magnitude_pu: 1.0,
phase_rad: 0.0,
power: 0.0,
},
];
let k = compute_k_factor(&harmonics);
assert!(
(k - 5.0).abs() < 1e-10,
"K-factor should be 5.0, got {k:.6}"
);
}
#[test]
fn test_transformer_derating_k1() {
let d = transformer_derating_factor(4.0, 4.0);
let expected = (1.0_f64 / (1.0 + 0.10)).sqrt();
assert!(
(d - expected).abs() < 1e-9,
"Derating should be {expected:.4}, got {d:.4}"
);
}
#[test]
fn test_transformer_derating_k1_trivial() {
let d = transformer_derating_factor(1.0, 1.0);
assert!(
d > 0.9 && d < 1.0,
"Derating for K=K_rated=1 should be ~0.95, got {d:.4}"
);
}
#[test]
fn test_extract_harmonics_length() {
let fs = 10_000.0_f64;
let f0 = 50.0_f64;
let n = (fs / f0 * 4.0) as usize;
let v = sine(1.0, f0, 0.0, fs, n);
let comps = extract_harmonics(&v, fs, f0, 10);
assert!(comps.len() <= 10);
assert!(!comps.is_empty());
}
#[test]
fn test_analyze_waveform_error_empty() {
let result = analyze_waveform(&[], &[], 10_000.0, 50.0, 10);
assert!(result.is_err());
}
#[test]
fn test_analyze_waveform_error_length_mismatch() {
let v = vec![0.0_f64; 100];
let i = vec![0.0_f64; 50];
let result = analyze_waveform(&v, &i, 10_000.0, 50.0, 10);
assert!(result.is_err());
}
#[test]
fn test_goertzel_amplitude() {
let fs = 10_000.0_f64;
let f0 = 50.0_f64;
let n = (fs / f0 * 10.0) as usize;
let v = sine(2.0, f0, 0.0, fs, n);
let (re, im) = goertzel(&v, f0, fs);
let peak = (re * re + im * im).sqrt();
assert!(
(peak - 2.0).abs() < 0.05,
"Goertzel peak amplitude should be ≈2.0, got {peak:.4}"
);
}
#[test]
fn test_interharmonic_detection_none_for_pure_sine() {
let fs = 10_000.0_f64;
let f0 = 50.0_f64;
let n = (fs / f0 * 4.0) as usize;
let v = sine(1.0, f0, 0.0, fs, n);
let spectrum: Vec<f64> = (0..n)
.map(|k| {
let (re, im) = goertzel(&v, k as f64 * fs / n as f64, fs);
(re * re + im * im).sqrt()
})
.take(n / 2) .collect();
let ih = detect_interharmonics(&spectrum, fs, f0, 0.05);
for (freq, _mag) in &ih {
let nearest = (freq / f0).round();
assert!(
(freq - nearest * f0).abs() > 1.0,
"Detected interharmonic at {freq:.1} Hz too close to harmonic"
);
}
}
}