use crate::powerquality::waveform::HarmonicComponent;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct En50160Limits {
pub thd_voltage_pct: f64,
pub individual_harmonics: Vec<(usize, f64)>,
pub voltage_variation_pu: f64,
pub frequency_variation_hz: f64,
pub flicker_pst_limit: f64,
pub unbalance_pct: f64,
}
impl En50160Limits {
pub fn standard() -> Self {
let individual_harmonics = vec![
(2, 2.0),
(3, 5.0),
(4, 1.0),
(5, 6.0),
(6, 0.5),
(7, 5.0),
(8, 0.5),
(9, 1.5),
(10, 0.5),
(11, 3.5),
(12, 0.5),
(13, 3.0),
(14, 0.5),
(15, 0.5),
(17, 2.0),
(19, 1.5),
(21, 0.5),
(23, 1.5),
(25, 1.5),
];
Self {
thd_voltage_pct: 8.0,
individual_harmonics,
voltage_variation_pu: 0.10,
frequency_variation_hz: 1.0,
flicker_pst_limit: 1.0,
unbalance_pct: 2.0,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct En50160Report {
pub thd_compliant: bool,
pub thd_measured_pct: f64,
pub individual_harmonics_compliant: Vec<(usize, bool, f64)>,
pub voltage_variation_compliant: bool,
pub frequency_compliant: bool,
pub flicker_compliant: bool,
pub unbalance_compliant: bool,
pub overall_compliant: bool,
pub observation_period_hours: f64,
}
pub fn check_en50160_compliance(
v_rms_timeline: &[f64],
frequency_timeline: &[f64],
harmonics: &[HarmonicComponent],
flicker_pst: f64,
unbalance_pct: f64,
limits: &En50160Limits,
) -> En50160Report {
let observation_period_hours = v_rms_timeline.len() as f64 / 6.0;
let v1 = harmonics
.first()
.map(|h| h.magnitude_pu)
.unwrap_or(1.0)
.max(1e-15);
let thd_sq: f64 = harmonics
.iter()
.skip(1)
.map(|h| h.magnitude_pu.powi(2))
.sum();
let thd_measured_pct = thd_sq.sqrt() / v1 * 100.0;
let thd_compliant = thd_measured_pct <= limits.thd_voltage_pct;
let individual_harmonics_compliant: Vec<(usize, bool, f64)> = limits
.individual_harmonics
.iter()
.map(|&(order, limit_pct)| {
let measured = harmonics
.iter()
.find(|h| h.order == order)
.map(|h| h.magnitude_pu / v1 * 100.0)
.unwrap_or(0.0);
(order, measured <= limit_pct, measured)
})
.collect();
let voltage_variation_compliant = if v_rms_timeline.is_empty() {
true
} else {
let n_within = v_rms_timeline
.iter()
.filter(|&&v| (v - 1.0).abs() <= limits.voltage_variation_pu)
.count();
(n_within as f64 / v_rms_timeline.len() as f64) >= 0.95
};
let frequency_compliant = if frequency_timeline.is_empty() {
true
} else {
let nominal_hz = 50.0_f64; let n_within = frequency_timeline
.iter()
.filter(|&&f| (f - nominal_hz).abs() <= limits.frequency_variation_hz)
.count();
(n_within as f64 / frequency_timeline.len() as f64) >= 0.995
};
let flicker_compliant = flicker_pst <= limits.flicker_pst_limit;
let unbalance_compliant = unbalance_pct <= limits.unbalance_pct;
let all_ih_compliant = individual_harmonics_compliant.iter().all(|(_, ok, _)| *ok);
let overall_compliant = thd_compliant
&& all_ih_compliant
&& voltage_variation_compliant
&& frequency_compliant
&& flicker_compliant
&& unbalance_compliant;
En50160Report {
thd_compliant,
thd_measured_pct,
individual_harmonics_compliant,
voltage_variation_compliant,
frequency_compliant,
flicker_compliant,
unbalance_compliant,
overall_compliant,
observation_period_hours,
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Ieee519Limits {
pub isc_il_ratio: f64,
pub odd_harmonics: Vec<(usize, f64)>,
pub thd_i_pct: f64,
}
impl Ieee519Limits {
pub fn for_isc_ratio(isc_il: f64) -> Self {
let (odd_harmonics, thd_i_pct) = if isc_il < 20.0 {
(
vec![
(10, 4.0),
(16, 2.0),
(22, 1.5),
(34, 0.6),
(usize::MAX, 0.3),
],
5.0,
)
} else if isc_il < 50.0 {
(
vec![
(10, 7.0),
(16, 3.5),
(22, 2.5),
(34, 1.0),
(usize::MAX, 0.5),
],
8.0,
)
} else if isc_il < 100.0 {
(
vec![
(10, 10.0),
(16, 4.5),
(22, 4.0),
(34, 1.5),
(usize::MAX, 0.7),
],
12.0,
)
} else if isc_il < 1000.0 {
(
vec![
(10, 12.0),
(16, 5.5),
(22, 5.0),
(34, 2.0),
(usize::MAX, 1.0),
],
15.0,
)
} else {
(
vec![
(10, 15.0),
(16, 7.0),
(22, 6.0),
(34, 2.5),
(usize::MAX, 1.4),
],
20.0,
)
};
Self {
isc_il_ratio: isc_il,
odd_harmonics,
thd_i_pct,
}
}
fn limit_for_order(&self, h: usize) -> f64 {
if h % 2 == 0 {
return self.limit_for_order(h + 1) * 0.25;
}
for &(max_order, limit) in &self.odd_harmonics {
if h <= max_order {
return limit;
}
}
self.odd_harmonics.last().map(|&(_, l)| l).unwrap_or(0.3)
}
}
pub fn check_ieee519_compliance(
current_harmonics: &[HarmonicComponent],
il_fundamental: f64,
limits: &Ieee519Limits,
) -> bool {
let il = il_fundamental.max(1e-15);
let tdd_sq: f64 = current_harmonics
.iter()
.skip(1)
.map(|h| h.magnitude_pu.powi(2))
.sum();
let tdd_pct = tdd_sq.sqrt() / il * 100.0;
if tdd_pct > limits.thd_i_pct {
return false;
}
for h in current_harmonics.iter().skip(1) {
let limit = limits.limit_for_order(h.order);
let measured_pct = h.magnitude_pu / il * 100.0;
if measured_pct > limit {
return false;
}
}
true
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PqIndices {
pub v_avg_pu: f64,
pub v_min_pu: f64,
pub v_max_pu: f64,
pub v_std_pu: f64,
pub percent_time_within_10pct: f64,
pub percent_time_within_5pct: f64,
pub n_exceedances_10pct: usize,
}
pub fn compute_pq_indices(v_rms_10min: &[f64], nominal_v: f64) -> PqIndices {
if v_rms_10min.is_empty() {
return PqIndices {
v_avg_pu: nominal_v,
v_min_pu: nominal_v,
v_max_pu: nominal_v,
v_std_pu: 0.0,
percent_time_within_10pct: 100.0,
percent_time_within_5pct: 100.0,
n_exceedances_10pct: 0,
};
}
let n = v_rms_10min.len() as f64;
let v_avg = v_rms_10min.iter().sum::<f64>() / n;
let v_min = v_rms_10min.iter().copied().fold(f64::INFINITY, f64::min);
let v_max = v_rms_10min
.iter()
.copied()
.fold(f64::NEG_INFINITY, f64::max);
let variance = v_rms_10min
.iter()
.map(|&v| (v - v_avg).powi(2))
.sum::<f64>()
/ n;
let v_std = variance.sqrt();
let n_within_10 = v_rms_10min
.iter()
.filter(|&&v| (v - nominal_v).abs() <= 0.10)
.count();
let n_within_5 = v_rms_10min
.iter()
.filter(|&&v| (v - nominal_v).abs() <= 0.05)
.count();
let n_exc_10 = v_rms_10min.len() - n_within_10;
PqIndices {
v_avg_pu: v_avg,
v_min_pu: v_min,
v_max_pu: v_max,
v_std_pu: v_std,
percent_time_within_10pct: n_within_10 as f64 / v_rms_10min.len() as f64 * 100.0,
percent_time_within_5pct: n_within_5 as f64 / v_rms_10min.len() as f64 * 100.0,
n_exceedances_10pct: n_exc_10,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::powerquality::waveform::HarmonicComponent;
fn make_harmonic(order: usize, mag: f64) -> HarmonicComponent {
HarmonicComponent {
order,
magnitude_pu: mag,
phase_rad: 0.0,
power: 0.0,
}
}
#[test]
fn test_en50160_compliant_network() {
let v_rms = vec![1.0_f64; 1008]; let freq = vec![50.0_f64; 6048]; let harmonics = vec![make_harmonic(1, 1.0)]; let limits = En50160Limits::standard();
let report = check_en50160_compliance(&v_rms, &freq, &harmonics, 0.3, 0.5, &limits);
assert!(
report.overall_compliant,
"Ideal network should be EN 50160 compliant"
);
assert!(report.thd_compliant);
assert!(report.voltage_variation_compliant);
assert!(report.frequency_compliant);
}
#[test]
fn test_en50160_thd_violation() {
let v_rms = vec![1.0_f64; 100];
let freq = vec![50.0_f64; 100];
let harmonics = vec![make_harmonic(1, 1.0), make_harmonic(3, 0.10)];
let limits = En50160Limits::standard();
let report = check_en50160_compliance(&v_rms, &freq, &harmonics, 0.3, 0.5, &limits);
assert!(!report.thd_compliant, "THD = 10% should fail the 8% limit");
assert!(!report.overall_compliant);
}
#[test]
fn test_en50160_voltage_variation_violation() {
let mut v_rms = vec![1.0_f64; 900];
v_rms.extend(vec![1.15_f64; 100]); let freq = vec![50.0_f64; 100];
let harmonics = vec![make_harmonic(1, 1.0)];
let limits = En50160Limits::standard();
let report = check_en50160_compliance(&v_rms, &freq, &harmonics, 0.3, 0.5, &limits);
assert!(!report.voltage_variation_compliant);
}
#[test]
fn test_ieee519_compliant_low_harmonics() {
let harmonics = vec![make_harmonic(1, 1.0)];
let limits = Ieee519Limits::for_isc_ratio(50.0);
assert!(check_ieee519_compliance(&harmonics, 1.0, &limits));
}
#[test]
fn test_ieee519_violation_high_harmonics() {
let harmonics = vec![
make_harmonic(1, 1.0),
make_harmonic(5, 0.06), ];
let limits = Ieee519Limits::for_isc_ratio(10.0);
assert!(
!check_ieee519_compliance(&harmonics, 1.0, &limits),
"6% 5th harmonic should violate IEEE 519 for ISC/IL < 20"
);
}
#[test]
fn test_pq_indices_range() {
let v: Vec<f64> = (0..100).map(|i| 0.98 + 0.04 * (i as f64 / 100.0)).collect();
let idx = compute_pq_indices(&v, 1.0);
assert!(idx.v_min_pu <= idx.v_avg_pu);
assert!(idx.v_avg_pu <= idx.v_max_pu);
assert!(idx.v_avg_pu > 0.0 && idx.v_avg_pu < 2.0);
assert!(idx.percent_time_within_10pct >= 0.0 && idx.percent_time_within_10pct <= 100.0);
assert!(idx.percent_time_within_5pct >= 0.0 && idx.percent_time_within_5pct <= 100.0);
}
#[test]
fn test_pq_indices_empty() {
let idx = compute_pq_indices(&[], 1.0);
assert_eq!(idx.v_avg_pu, 1.0);
assert_eq!(idx.n_exceedances_10pct, 0);
}
#[test]
fn test_pq_indices_all_nominal() {
let v = vec![1.0_f64; 200];
let idx = compute_pq_indices(&v, 1.0);
assert_eq!(idx.percent_time_within_10pct, 100.0);
assert_eq!(idx.percent_time_within_5pct, 100.0);
assert_eq!(idx.n_exceedances_10pct, 0);
assert!((idx.v_std_pu).abs() < 1e-12);
}
#[test]
fn test_ieee519_limits_isc_ratio_boundaries() {
for &ratio in &[5.0, 30.0, 75.0, 500.0, 2000.0] {
let limits = Ieee519Limits::for_isc_ratio(ratio);
assert!(limits.thd_i_pct > 0.0);
assert!(!limits.odd_harmonics.is_empty());
}
}
#[test]
fn test_en50160_flicker_violation() {
let v_rms = vec![1.0_f64; 100];
let freq = vec![50.0_f64; 100];
let harmonics = vec![make_harmonic(1, 1.0)];
let limits = En50160Limits::standard();
let report = check_en50160_compliance(&v_rms, &freq, &harmonics, 1.5, 0.5, &limits);
assert!(!report.flicker_compliant);
assert!(!report.overall_compliant);
}
#[test]
fn test_thd_zero_from_pure_fundamental_harmonics() {
let v_rms = vec![1.0_f64; 100];
let freq = vec![50.0_f64; 100];
let harmonics = vec![make_harmonic(1, 1.0)];
let limits = En50160Limits::standard();
let report = check_en50160_compliance(&v_rms, &freq, &harmonics, 0.3, 0.5, &limits);
assert!(
report.thd_measured_pct < 1e-9,
"pure fundamental should give THD = 0%"
);
}
#[test]
fn test_thd_known_value_5pct_5th_harmonic() {
let v_rms = vec![1.0_f64; 100];
let freq = vec![50.0_f64; 100];
let harmonics = vec![make_harmonic(1, 1.0), make_harmonic(5, 0.05)];
let limits = En50160Limits::standard();
let report = check_en50160_compliance(&v_rms, &freq, &harmonics, 0.3, 0.5, &limits);
assert!(
(report.thd_measured_pct - 5.0).abs() < 0.001,
"THD should be 5.0%, got {}",
report.thd_measured_pct
);
}
#[test]
fn test_k_factor_pure_fundamental() {
let harmonics = vec![HarmonicComponent {
order: 1,
magnitude_pu: 1.0,
phase_rad: 0.0,
power: 0.0,
}];
let k = crate::powerquality::waveform::compute_k_factor(&harmonics);
assert!(
(k - 1.0).abs() < 1e-9,
"K-factor for pure fundamental should be 1.0, got {k}"
);
}
#[test]
fn test_crest_factor_pure_sine_via_waveform() {
let sample_rate = 10_000.0_f64;
let freq = 50.0_f64;
let n_samples = (5.0 * sample_rate / freq) as usize; let wave: Vec<f64> = (0..n_samples)
.map(|i| (2.0 * std::f64::consts::PI * freq * i as f64 / sample_rate).sin())
.collect();
let metrics =
crate::powerquality::waveform::analyze_waveform(&wave, &wave, sample_rate, freq, 10)
.expect("analyze_waveform failed");
assert!(
(metrics.crest_factor - std::f64::consts::SQRT_2).abs() < 0.05,
"Crest factor for pure sine should be sqrt(2) ≈ {}, got {}",
std::f64::consts::SQRT_2,
metrics.crest_factor
);
}
#[test]
fn test_ieee519_tdd_violation() {
let harmonics = vec![
make_harmonic(1, 1.0),
make_harmonic(5, 0.04),
make_harmonic(7, 0.04),
];
let limits = Ieee519Limits::for_isc_ratio(10.0); assert!(
!check_ieee519_compliance(&harmonics, 1.0, &limits),
"TDD ≈ 5.66% should violate the 5% limit"
);
}
}