use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub enum Iec61000Class {
ClassA,
ClassB,
ClassC,
ClassD,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HarmonicLimit {
pub harmonic: u32,
pub limit: f64,
}
pub fn iec61000_3_2_class_a() -> Vec<HarmonicLimit> {
vec![
HarmonicLimit {
harmonic: 3,
limit: 2.30,
},
HarmonicLimit {
harmonic: 5,
limit: 1.14,
},
HarmonicLimit {
harmonic: 7,
limit: 0.77,
},
HarmonicLimit {
harmonic: 9,
limit: 0.40,
},
HarmonicLimit {
harmonic: 11,
limit: 0.33,
},
HarmonicLimit {
harmonic: 13,
limit: 0.21,
},
HarmonicLimit {
harmonic: 2,
limit: 1.08,
},
HarmonicLimit {
harmonic: 4,
limit: 0.43,
},
HarmonicLimit {
harmonic: 6,
limit: 0.30,
},
HarmonicLimit {
harmonic: 8,
limit: 0.23,
},
HarmonicLimit {
harmonic: 10,
limit: 0.184,
},
HarmonicLimit {
harmonic: 12,
limit: 0.153,
},
]
}
pub fn iec61000_3_2_class_c(i_fundamental: f64) -> Vec<HarmonicLimit> {
let pct = [
(3u32, 30.0_f64),
(5, 10.0),
(7, 7.0),
(9, 5.0),
(11, 3.0),
(13, 3.0),
];
pct.iter()
.map(|&(h, p)| HarmonicLimit {
harmonic: h,
limit: i_fundamental * p / 100.0,
})
.collect()
}
pub fn iec61000_3_2_class_d_maw() -> Vec<HarmonicLimit> {
vec![
HarmonicLimit {
harmonic: 3,
limit: 3.40,
},
HarmonicLimit {
harmonic: 5,
limit: 1.90,
},
HarmonicLimit {
harmonic: 7,
limit: 1.00,
},
HarmonicLimit {
harmonic: 9,
limit: 0.50,
},
HarmonicLimit {
harmonic: 11,
limit: 0.35,
},
HarmonicLimit {
harmonic: 13,
limit: 0.296,
},
]
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Ieee519VoltageLimits {
pub voltage_level: String,
pub individual_limit_pct: f64,
pub thd_limit_pct: f64,
}
pub fn ieee519_voltage_limits() -> Vec<Ieee519VoltageLimits> {
vec![
Ieee519VoltageLimits {
voltage_level: "≤1 kV".to_string(),
individual_limit_pct: 5.0,
thd_limit_pct: 8.0,
},
Ieee519VoltageLimits {
voltage_level: "1–69 kV".to_string(),
individual_limit_pct: 3.0,
thd_limit_pct: 5.0,
},
Ieee519VoltageLimits {
voltage_level: "69–161 kV".to_string(),
individual_limit_pct: 1.5,
thd_limit_pct: 2.5,
},
Ieee519VoltageLimits {
voltage_level: ">161 kV".to_string(),
individual_limit_pct: 1.0,
thd_limit_pct: 1.5,
},
]
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Ieee519CurrentLimits {
pub isc_il_ratio: String,
pub h_lt_11_pct: f64,
pub h_11_17_pct: f64,
pub h_17_23_pct: f64,
pub h_23_35_pct: f64,
pub h_ge_35_pct: f64,
pub tdd_pct: f64,
}
pub fn ieee519_current_limits() -> Vec<Ieee519CurrentLimits> {
vec![
Ieee519CurrentLimits {
isc_il_ratio: "<20".to_string(),
h_lt_11_pct: 4.0,
h_11_17_pct: 2.0,
h_17_23_pct: 1.5,
h_23_35_pct: 0.6,
h_ge_35_pct: 0.3,
tdd_pct: 5.0,
},
Ieee519CurrentLimits {
isc_il_ratio: "20–50".to_string(),
h_lt_11_pct: 7.0,
h_11_17_pct: 3.5,
h_17_23_pct: 2.5,
h_23_35_pct: 1.0,
h_ge_35_pct: 0.5,
tdd_pct: 8.0,
},
Ieee519CurrentLimits {
isc_il_ratio: "50–100".to_string(),
h_lt_11_pct: 10.0,
h_11_17_pct: 4.5,
h_17_23_pct: 4.0,
h_23_35_pct: 1.5,
h_ge_35_pct: 0.7,
tdd_pct: 12.0,
},
Ieee519CurrentLimits {
isc_il_ratio: "100–1000".to_string(),
h_lt_11_pct: 12.0,
h_11_17_pct: 5.5,
h_17_23_pct: 5.0,
h_23_35_pct: 2.0,
h_ge_35_pct: 1.0,
tdd_pct: 15.0,
},
Ieee519CurrentLimits {
isc_il_ratio: ">1000".to_string(),
h_lt_11_pct: 15.0,
h_11_17_pct: 7.0,
h_17_23_pct: 6.0,
h_23_35_pct: 2.5,
h_ge_35_pct: 1.4,
tdd_pct: 20.0,
},
]
}
pub fn ieee519_current_limit_for_ratio(isc_il: f64) -> Ieee519CurrentLimits {
if isc_il < 20.0 {
Ieee519CurrentLimits {
isc_il_ratio: "<20".to_string(),
h_lt_11_pct: 4.0,
h_11_17_pct: 2.0,
h_17_23_pct: 1.5,
h_23_35_pct: 0.6,
h_ge_35_pct: 0.3,
tdd_pct: 5.0,
}
} else if isc_il < 50.0 {
Ieee519CurrentLimits {
isc_il_ratio: "20-50".to_string(),
h_lt_11_pct: 7.0,
h_11_17_pct: 3.5,
h_17_23_pct: 2.5,
h_23_35_pct: 1.0,
h_ge_35_pct: 0.5,
tdd_pct: 8.0,
}
} else if isc_il < 100.0 {
Ieee519CurrentLimits {
isc_il_ratio: "50-100".to_string(),
h_lt_11_pct: 10.0,
h_11_17_pct: 4.5,
h_17_23_pct: 4.0,
h_23_35_pct: 1.5,
h_ge_35_pct: 0.7,
tdd_pct: 12.0,
}
} else if isc_il < 1000.0 {
Ieee519CurrentLimits {
isc_il_ratio: "100-1000".to_string(),
h_lt_11_pct: 12.0,
h_11_17_pct: 5.5,
h_17_23_pct: 5.0,
h_23_35_pct: 2.0,
h_ge_35_pct: 1.0,
tdd_pct: 15.0,
}
} else {
Ieee519CurrentLimits {
isc_il_ratio: ">1000".to_string(),
h_lt_11_pct: 15.0,
h_11_17_pct: 7.0,
h_17_23_pct: 6.0,
h_23_35_pct: 2.5,
h_ge_35_pct: 1.4,
tdd_pct: 20.0,
}
}
}
pub fn check_iec61000_3_2_class_a(measured: &[(u32, f64)]) -> Vec<String> {
let limits = iec61000_3_2_class_a();
let mut violations = Vec::new();
for &(h, i_meas) in measured {
if let Some(lim) = limits.iter().find(|l| l.harmonic == h) {
if i_meas > lim.limit {
violations.push(format!(
"H{}: {:.3} A exceeds limit {:.3} A",
h, i_meas, lim.limit
));
}
}
}
violations
}
pub fn check_ieee519_voltage(
bus_kv: f64,
harmonics_pct: &[(u32, f64)],
thd_pct: f64,
) -> Vec<String> {
let limits = ieee519_voltage_limits();
let lim = if bus_kv <= 1.0 {
&limits[0]
} else if bus_kv <= 69.0 {
&limits[1]
} else if bus_kv <= 161.0 {
&limits[2]
} else {
&limits[3]
};
let mut violations = Vec::new();
for &(h, v_pct) in harmonics_pct {
if v_pct > lim.individual_limit_pct {
violations.push(format!(
"H{}: {:.2}% exceeds individual limit {:.1}% ({})",
h, v_pct, lim.individual_limit_pct, lim.voltage_level
));
}
}
if thd_pct > lim.thd_limit_pct {
violations.push(format!(
"THD: {:.2}% exceeds limit {:.1}% ({})",
thd_pct, lim.thd_limit_pct, lim.voltage_level
));
}
violations
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_class_a_limits_not_empty() {
let limits = iec61000_3_2_class_a();
assert!(!limits.is_empty());
assert!(limits.iter().any(|l| l.harmonic == 3));
assert!(limits.iter().any(|l| l.harmonic == 5));
}
#[test]
fn test_class_a_compliant() {
let measured = vec![(3u32, 1.0_f64), (5, 0.5), (7, 0.3)];
let violations = check_iec61000_3_2_class_a(&measured);
assert!(
violations.is_empty(),
"Should be compliant: {:?}",
violations
);
}
#[test]
fn test_class_a_violation() {
let measured = vec![(3u32, 3.0_f64)]; let violations = check_iec61000_3_2_class_a(&measured);
assert!(!violations.is_empty(), "Should detect violation");
}
#[test]
fn test_class_c_limits_scale_with_current() {
let limits_1a = iec61000_3_2_class_c(1.0);
let limits_10a = iec61000_3_2_class_c(10.0);
for (l1, l10) in limits_1a.iter().zip(limits_10a.iter()) {
assert!((l10.limit / l1.limit - 10.0).abs() < 1e-10);
}
}
#[test]
fn test_ieee519_voltage_limits_count() {
assert_eq!(ieee519_voltage_limits().len(), 4);
}
#[test]
fn test_ieee519_voltage_compliant() {
let harmonics = vec![(3u32, 1.0_f64), (5, 0.8), (7, 0.5)];
let violations = check_ieee519_voltage(11.0, &harmonics, 2.0);
assert!(
violations.is_empty(),
"Should be compliant: {:?}",
violations
);
}
#[test]
fn test_ieee519_voltage_violation() {
let harmonics = vec![(3u32, 4.0_f64)]; let violations = check_ieee519_voltage(11.0, &harmonics, 6.0);
assert!(!violations.is_empty(), "Should detect violations");
}
#[test]
fn test_ieee519_current_limits_count() {
assert_eq!(ieee519_current_limits().len(), 5);
}
#[test]
fn test_ieee519_current_limit_for_ratio() {
let lim_low = ieee519_current_limit_for_ratio(10.0);
let lim_high = ieee519_current_limit_for_ratio(2000.0);
assert!(
lim_high.h_lt_11_pct > lim_low.h_lt_11_pct,
"Higher Isc/IL → higher allowable distortion"
);
}
#[test]
fn test_class_d_limits_not_empty() {
let limits = iec61000_3_2_class_d_maw();
assert!(!limits.is_empty());
for l in &limits {
assert!(l.limit > 0.0);
}
}
#[test]
fn test_ieee519_low_voltage_bus() {
let lims = ieee519_voltage_limits();
assert!((lims[0].thd_limit_pct - 8.0).abs() < 1e-10);
}
}