use crate::error::{OxiGridError, Result};
use std::f64::consts::PI;
pub type HarmonicOrder = u32;
#[derive(Debug, Clone, PartialEq)]
pub enum HarmonicSourceType {
SixPulseDrive,
TwelvePulseDrive,
ArcFurnace,
TransformerInrush,
SwitchModePowerSupply,
PhotovoltaicInverter,
ArduinoInverter,
}
impl HarmonicSourceType {
pub fn spectrum(&self, fundamental_current_pu: f64) -> Vec<(HarmonicOrder, f64, f64)> {
match self {
HarmonicSourceType::SixPulseDrive => vec![
(5, fundamental_current_pu * 0.175, -PI / 6.0),
(7, fundamental_current_pu * 0.111, PI / 6.0),
(11, fundamental_current_pu * 0.045, -PI / 6.0),
(13, fundamental_current_pu * 0.029, PI / 6.0),
(17, fundamental_current_pu * 0.015, -PI / 6.0),
(19, fundamental_current_pu * 0.010, PI / 6.0),
(23, fundamental_current_pu * 0.009, -PI / 6.0),
(25, fundamental_current_pu * 0.008, PI / 6.0),
],
HarmonicSourceType::TwelvePulseDrive => vec![
(11, fundamental_current_pu * 0.076, -PI / 12.0),
(13, fundamental_current_pu * 0.059, PI / 12.0),
(23, fundamental_current_pu * 0.020, -PI / 12.0),
(25, fundamental_current_pu * 0.018, PI / 12.0),
],
HarmonicSourceType::ArduinoInverter => vec![
(3, fundamental_current_pu * 0.30, -PI / 4.0),
(5, fundamental_current_pu * 0.15, PI / 4.0),
(7, fundamental_current_pu * 0.07, -PI / 4.0),
(9, fundamental_current_pu * 0.03, PI / 4.0),
],
HarmonicSourceType::ArcFurnace => vec![
(2, fundamental_current_pu * 0.06, 0.0),
(3, fundamental_current_pu * 0.08, 0.0),
(4, fundamental_current_pu * 0.03, 0.0),
(5, fundamental_current_pu * 0.06, 0.0),
(7, fundamental_current_pu * 0.04, 0.0),
(9, fundamental_current_pu * 0.02, 0.0),
(11, fundamental_current_pu * 0.015, 0.0),
(13, fundamental_current_pu * 0.010, 0.0),
],
HarmonicSourceType::TransformerInrush => vec![
(2, fundamental_current_pu * 0.50, 0.0),
(3, fundamental_current_pu * 0.20, PI / 3.0),
(4, fundamental_current_pu * 0.10, 0.0),
(5, fundamental_current_pu * 0.05, PI / 3.0),
],
HarmonicSourceType::SwitchModePowerSupply => vec![
(3, fundamental_current_pu * 0.85, PI / 2.0),
(5, fundamental_current_pu * 0.52, -PI / 2.0),
(7, fundamental_current_pu * 0.19, PI / 2.0),
(9, fundamental_current_pu * 0.10, -PI / 2.0),
(11, fundamental_current_pu * 0.06, PI / 2.0),
(13, fundamental_current_pu * 0.04, -PI / 2.0),
],
HarmonicSourceType::PhotovoltaicInverter => vec![
(5, fundamental_current_pu * 0.04, -PI / 6.0),
(7, fundamental_current_pu * 0.03, PI / 6.0),
(11, fundamental_current_pu * 0.015, -PI / 6.0),
(13, fundamental_current_pu * 0.010, PI / 6.0),
],
}
}
}
#[derive(Debug, Clone)]
pub struct HarmonicSource {
pub bus: usize,
pub harmonic_currents: Vec<(HarmonicOrder, f64, f64)>,
pub source_type: HarmonicSourceType,
pub fundamental_current_pu: f64,
}
impl HarmonicSource {
pub fn new(bus: usize, source_type: HarmonicSourceType, fundamental_current_pu: f64) -> Self {
let harmonic_currents = source_type.spectrum(fundamental_current_pu);
Self {
bus,
harmonic_currents,
source_type,
fundamental_current_pu,
}
}
pub fn with_explicit_currents(
bus: usize,
source_type: HarmonicSourceType,
harmonic_currents: Vec<(HarmonicOrder, f64, f64)>,
) -> Self {
let fundamental_current_pu = harmonic_currents
.iter()
.find(|(h, _, _)| *h == 1)
.map(|(_, mag, _)| *mag)
.unwrap_or(1.0);
Self {
bus,
harmonic_currents,
source_type,
fundamental_current_pu,
}
}
pub fn injection_at(&self, h: HarmonicOrder) -> (f64, f64) {
for &(order, mag, angle) in &self.harmonic_currents {
if order == h {
return (mag * angle.cos(), mag * angle.sin());
}
}
(0.0, 0.0)
}
}
#[derive(Debug, Clone)]
pub struct HarmonicBranch {
pub from: usize,
pub to: usize,
pub r_fundamental_pu: f64,
pub x_fundamental_pu: f64,
pub b_fundamental_pu: f64,
pub r_skin_effect_exp: f64,
pub rating_mva: f64,
}
impl HarmonicBranch {
pub fn new(
from: usize,
to: usize,
r_fundamental_pu: f64,
x_fundamental_pu: f64,
b_fundamental_pu: f64,
rating_mva: f64,
) -> Self {
Self {
from,
to,
r_fundamental_pu,
x_fundamental_pu,
b_fundamental_pu,
r_skin_effect_exp: 0.5,
rating_mva,
}
}
pub fn impedance_at_harmonic(&self, h: HarmonicOrder) -> (f64, f64) {
let hf = h as f64;
let r_h = self.r_fundamental_pu * hf.powf(self.r_skin_effect_exp);
let x_h = self.x_fundamental_pu * hf;
(r_h, x_h)
}
pub fn admittance_at_harmonic(&self, h: HarmonicOrder) -> (f64, f64) {
let (r, x) = self.impedance_at_harmonic(h);
let denom = r * r + x * x;
if denom < 1e-30 {
(0.0, 0.0)
} else {
(r / denom, -x / denom)
}
}
pub fn shunt_susceptance_at_harmonic(&self, h: HarmonicOrder) -> f64 {
self.b_fundamental_pu * h as f64
}
}
#[derive(Debug, Clone)]
pub enum HarmonicLoadType {
LinearResistive,
NonLinear(HarmonicSourceType),
PassiveFilter {
tuned_order: HarmonicOrder,
q_factor: f64,
},
ActiveFilter,
}
#[derive(Debug, Clone)]
pub struct HarmonicLoad {
pub bus: usize,
pub p_fundamental_mw: f64,
pub q_fundamental_mvar: f64,
pub load_type: HarmonicLoadType,
}
#[derive(Debug, Clone)]
pub struct HarmonicPfConfigV1 {
pub base_mva: f64,
pub fundamental_hz: f64,
pub harmonic_orders: Vec<HarmonicOrder>,
pub max_iterations: usize,
pub tolerance_pu: f64,
pub nominal_voltage_kv: f64,
}
impl Default for HarmonicPfConfigV1 {
fn default() -> Self {
Self {
base_mva: 100.0,
fundamental_hz: 50.0,
harmonic_orders: vec![3, 5, 7, 11, 13, 17, 19, 23, 25],
max_iterations: 50,
tolerance_pu: 1e-6,
nominal_voltage_kv: 20.0,
}
}
}
#[derive(Debug, Clone)]
pub struct HarmonicBusResult {
pub bus: usize,
pub harmonic: HarmonicOrder,
pub voltage_magnitude_pu: f64,
pub voltage_angle_rad: f64,
pub thd_v: f64,
pub thd_i: f64,
}
#[derive(Debug, Clone)]
pub struct IeeComplianceResult {
pub bus: usize,
pub voltage_thd_limit_pct: f64,
pub voltage_thd_actual_pct: f64,
pub current_thd_limit_pct: f64,
pub current_thd_actual_pct: f64,
pub compliant: bool,
}
#[derive(Debug, Clone)]
pub struct HarmonicPfResultV1 {
pub fundamental_voltages: Vec<f64>,
pub fundamental_angles: Vec<f64>,
pub harmonic_voltages: Vec<Vec<(HarmonicOrder, f64, f64)>>,
pub bus_thd_v: Vec<f64>,
pub bus_thd_i: Vec<f64>,
pub branch_harmonic_flows: Vec<Vec<(HarmonicOrder, f64)>>,
pub total_harmonic_losses_mw: f64,
pub ieee519_compliance: Vec<IeeComplianceResult>,
}
type Cx = (f64, f64);
#[inline]
fn cx_add(a: Cx, b: Cx) -> Cx {
(a.0 + b.0, a.1 + b.1)
}
#[inline]
fn cx_sub(a: Cx, b: Cx) -> Cx {
(a.0 - b.0, a.1 - b.1)
}
#[inline]
fn cx_mul(a: Cx, b: Cx) -> Cx {
(a.0 * b.0 - a.1 * b.1, a.0 * b.1 + a.1 * b.0)
}
#[inline]
fn cx_div(a: Cx, b: Cx) -> Cx {
let d = b.0 * b.0 + b.1 * b.1;
if d < 1e-300 {
(0.0, 0.0)
} else {
((a.0 * b.0 + a.1 * b.1) / d, (a.1 * b.0 - a.0 * b.1) / d)
}
}
#[inline]
fn cx_abs(a: Cx) -> f64 {
(a.0 * a.0 + a.1 * a.1).sqrt()
}
#[allow(clippy::needless_range_loop)]
fn complex_gaussian_elimination(a_matrix: &[Cx], b_vec: &[Cx], n: usize) -> Result<Vec<Cx>> {
if n == 0 {
return Err(OxiGridError::InvalidParameter(
"harmonic system size must be > 0".into(),
));
}
if a_matrix.len() != n * n || b_vec.len() != n {
return Err(OxiGridError::InvalidParameter(
"harmonic system dimension mismatch".into(),
));
}
let mut aug: Vec<Vec<Cx>> = (0..n)
.map(|i| {
let mut row: Vec<Cx> = (0..n).map(|j| a_matrix[i * n + j]).collect();
row.push(b_vec[i]);
row
})
.collect();
for col in 0..n {
let mut max_val = cx_abs(aug[col][col]);
let mut max_row = col;
for row in (col + 1)..n {
let v = cx_abs(aug[row][col]);
if v > max_val {
max_val = v;
max_row = row;
}
}
if max_val < 1e-20 {
return Err(OxiGridError::LinearAlgebra(format!(
"harmonic Y-matrix is singular at column {col} (near-zero pivot)"
)));
}
aug.swap(col, max_row);
let pivot = aug[col][col];
for row in (col + 1)..n {
let factor = cx_div(aug[row][col], pivot);
for j in col..=n {
let sub = cx_mul(factor, aug[col][j]);
aug[row][j] = cx_sub(aug[row][j], sub);
}
}
}
let mut x = vec![(0.0_f64, 0.0_f64); n];
for i in (0..n).rev() {
let mut s = aug[i][n];
for j in (i + 1)..n {
s = cx_sub(s, cx_mul(aug[i][j], x[j]));
}
x[i] = cx_div(s, aug[i][i]);
}
Ok(x)
}
pub struct HarmonicPowerFlow {
pub n_buses: usize,
pub branches: Vec<HarmonicBranch>,
pub sources: Vec<HarmonicSource>,
pub loads: Vec<HarmonicLoad>,
pub config: HarmonicPfConfigV1,
pub slack_bus: usize,
}
impl HarmonicPowerFlow {
pub fn new(
n_buses: usize,
branches: Vec<HarmonicBranch>,
sources: Vec<HarmonicSource>,
loads: Vec<HarmonicLoad>,
config: HarmonicPfConfigV1,
slack_bus: usize,
) -> Self {
Self {
n_buses,
branches,
sources,
loads,
config,
slack_bus,
}
}
pub fn build_admittance_matrix(&self, h: HarmonicOrder) -> Vec<Vec<Cx>> {
let n = self.n_buses;
let mut y = vec![vec![(0.0_f64, 0.0_f64); n]; n];
for branch in &self.branches {
let (g_series, b_series) = branch.admittance_at_harmonic(h);
let b_shunt_half = branch.shunt_susceptance_at_harmonic(h) * 0.5;
let fi = branch.from;
let ti = branch.to;
y[fi][ti] = cx_sub(y[fi][ti], (g_series, b_series));
y[ti][fi] = cx_sub(y[ti][fi], (g_series, b_series));
y[fi][fi] = cx_add(y[fi][fi], (g_series, b_series));
y[ti][ti] = cx_add(y[ti][ti], (g_series, b_series));
y[fi][fi] = cx_add(y[fi][fi], (0.0, b_shunt_half));
y[ti][ti] = cx_add(y[ti][ti], (0.0, b_shunt_half));
}
for load in &self.loads {
if let HarmonicLoadType::PassiveFilter {
tuned_order,
q_factor,
} = load.load_type
{
let bus = load.bus;
let q_load_pu = if load.q_fundamental_mvar.abs() > 1e-9 {
load.q_fundamental_mvar / self.config.base_mva
} else {
0.01 };
let x_c1 = 1.0 / q_load_pu.abs().max(1e-9);
let ht = tuned_order as f64;
let x_l1 = x_c1 / (ht * ht);
let r_f = ht * x_l1 / q_factor.max(1.0);
let hf = h as f64;
let x_l_h = x_l1 * hf;
let x_c_h = x_c1 / hf;
let x_net = x_l_h - x_c_h;
let z_f_r = r_f;
let z_f_x = x_net;
let denom = r_f * r_f + x_net * x_net;
let (g_f, b_f) = if denom > 1e-30 {
(z_f_r / denom, -z_f_x / denom)
} else {
(1e6, 0.0) };
y[bus][bus] = cx_add(y[bus][bus], (g_f, b_f));
}
}
y
}
fn solve_fundamental(&self, p_inj: &[f64], q_inj: &[f64]) -> Result<(Vec<f64>, Vec<f64>)> {
if p_inj.len() != self.n_buses || q_inj.len() != self.n_buses {
return Err(OxiGridError::InvalidParameter(
"p_inj/q_inj length must equal n_buses".into(),
));
}
let n = self.n_buses;
let y = self.build_admittance_matrix(1);
let mut v_re: Vec<f64> = vec![1.0; n];
let mut v_im: Vec<f64> = vec![0.0; n];
v_re[self.slack_bus] = 1.0;
v_im[self.slack_bus] = 0.0;
for _iter in 0..self.config.max_iterations {
let v_re_prev = v_re.clone();
let v_im_prev = v_im.clone();
let mut max_delta: f64 = 0.0;
for i in 0..n {
if i == self.slack_bus {
continue;
}
let vi_re = v_re[i];
let vi_im = v_im[i];
let vi_mag2 = vi_re * vi_re + vi_im * vi_im;
if vi_mag2 < 1e-20 {
continue;
}
let i_inj_re = (p_inj[i] * vi_re + q_inj[i] * vi_im) / vi_mag2;
let i_inj_im = (p_inj[i] * vi_im - q_inj[i] * vi_re) / vi_mag2;
let (y_ii_g, y_ii_b) = y[i][i];
let mut rhs_re = i_inj_re;
let mut rhs_im = i_inj_im;
for j in 0..n {
if j == i {
continue;
}
let (y_ij_g, y_ij_b) = y[i][j];
rhs_re -= y_ij_g * v_re[j] - y_ij_b * v_im[j];
rhs_im -= y_ij_g * v_im[j] + y_ij_b * v_re[j];
}
let (new_re, new_im) = cx_div((rhs_re, rhs_im), (y_ii_g, y_ii_b));
let delta = ((new_re - vi_re).powi(2) + (new_im - vi_im).powi(2)).sqrt();
max_delta = max_delta.max(delta);
v_re[i] = new_re;
v_im[i] = new_im;
}
let _ = v_re_prev;
let _ = v_im_prev;
if max_delta < self.config.tolerance_pu {
break;
}
}
let v_mag: Vec<f64> = (0..n)
.map(|i| (v_re[i] * v_re[i] + v_im[i] * v_im[i]).sqrt())
.collect();
let v_ang: Vec<f64> = (0..n).map(|i| v_im[i].atan2(v_re[i])).collect();
Ok((v_mag, v_ang))
}
pub fn harmonic_injection(&self, bus: usize, h: HarmonicOrder) -> Cx {
let mut i_re = 0.0_f64;
let mut i_im = 0.0_f64;
for src in &self.sources {
if src.bus == bus {
let (ir, ii) = src.injection_at(h);
i_re += ir;
i_im += ii;
}
}
for load in &self.loads {
if load.bus == bus {
if let HarmonicLoadType::NonLinear(ref src_type) = load.load_type {
let s_fund = (load.p_fundamental_mw.powi(2) + load.q_fundamental_mvar.powi(2))
.sqrt()
/ self.config.base_mva;
let spectrum = src_type.spectrum(s_fund);
for (order, mag, angle) in &spectrum {
if *order == h {
i_re += mag * angle.cos();
i_im += mag * angle.sin();
}
}
}
}
}
(i_re, i_im)
}
pub fn solve_harmonic(
&self,
h: HarmonicOrder,
y_matrix: &[Vec<Cx>],
i_injections: &[Cx],
) -> Result<Vec<Cx>> {
let n = self.n_buses;
if y_matrix.len() != n || i_injections.len() != n {
return Err(OxiGridError::InvalidParameter(
"Y-matrix or injection vector dimension mismatch".into(),
));
}
let mut a_flat: Vec<Cx> = Vec::with_capacity(n * n);
let mut b: Vec<Cx> = i_injections.to_vec();
for row in y_matrix.iter() {
for &entry in row.iter() {
a_flat.push(entry);
}
}
let s = self.slack_bus;
for j in 0..n {
a_flat[s * n + j] = if j == s { (1.0, 0.0) } else { (0.0, 0.0) };
}
b[s] = (0.0, 0.0);
let _ = h;
complex_gaussian_elimination(&a_flat, &b, n)
}
pub fn compute_voltage_thd(
v_fundamental: f64,
harmonic_voltages: &[(HarmonicOrder, f64, f64)],
) -> f64 {
if v_fundamental < 1e-10 {
return 0.0;
}
let sum_sq: f64 = harmonic_voltages
.iter()
.filter(|(h, _, _)| *h >= 2)
.map(|(_, mag, _)| mag * mag)
.sum();
(sum_sq.sqrt() / v_fundamental) * 100.0
}
pub fn ieee519_voltage_limit(voltage_kv: f64) -> f64 {
if voltage_kv <= 1.0 {
8.0
} else if voltage_kv <= 69.0 {
5.0
} else if voltage_kv <= 161.0 {
2.5
} else {
1.5
}
}
pub fn ieee519_current_limit(_voltage_kv: f64) -> f64 {
5.0
}
pub fn find_resonance_frequencies(&self, bus: usize) -> Vec<HarmonicOrder> {
let mut resonant: Vec<HarmonicOrder> = Vec::new();
let threshold = 0.01_f64;
for h in 2u32..=50 {
let y = self.build_admittance_matrix(h);
let (_, b_ii) = y[bus][bus];
if b_ii.abs() < threshold {
resonant.push(h);
}
}
resonant
}
pub fn design_passive_filter(
target_order: HarmonicOrder,
v_bus_kv: f64,
reactive_power_mvar: f64,
fundamental_hz: f64,
) -> (f64, f64) {
let omega1 = 2.0 * PI * fundamental_hz;
let omega_h = (target_order as f64) * omega1;
let v_volts = v_bus_kv * 1000.0;
let q_var = reactive_power_mvar * 1e6;
let c_farad = if v_volts.abs() < 1.0 || omega1 < 1e-6 {
0.0
} else {
q_var / (v_volts * v_volts * omega1)
};
let l_henry = if c_farad < 1e-30 || omega_h < 1e-6 {
0.0
} else {
1.0 / (c_farad * omega_h * omega_h)
};
(l_henry, c_farad)
}
pub fn solve(&self, p_injections: &[f64], q_injections: &[f64]) -> Result<HarmonicPfResultV1> {
let n = self.n_buses;
let (v_fund_mag, v_fund_ang) = self.solve_fundamental(p_injections, q_injections)?;
let mut harmonic_v: Vec<Vec<(HarmonicOrder, f64, f64)>> = vec![Vec::new(); n];
for &h in &self.config.harmonic_orders {
let y_h = self.build_admittance_matrix(h);
let i_h: Vec<Cx> = (0..n).map(|bus| self.harmonic_injection(bus, h)).collect();
let v_h = self.solve_harmonic(h, &y_h, &i_h)?;
for (bus, &(vr, vi)) in v_h.iter().enumerate() {
let mag = (vr * vr + vi * vi).sqrt();
let angle = vi.atan2(vr);
harmonic_v[bus].push((h, mag, angle));
}
}
let bus_thd_v: Vec<f64> = (0..n)
.map(|bus| Self::compute_voltage_thd(v_fund_mag[bus], &harmonic_v[bus]))
.collect();
let bus_thd_i: Vec<f64> = (0..n)
.map(|bus| {
let p = if bus < p_injections.len() {
p_injections[bus]
} else {
0.0
};
let q = if bus < q_injections.len() {
q_injections[bus]
} else {
0.0
};
let s_fund = (p * p + q * q).sqrt();
let v_f = v_fund_mag[bus].max(1e-6);
let i_fund = s_fund / v_f;
if i_fund < 1e-10 {
return 0.0;
}
let sum_sq: f64 = harmonic_v[bus]
.iter()
.filter(|(h_ord, _, _)| *h_ord >= 2)
.map(|(h_ord, v_mag, _)| {
let y_h = self.build_admittance_matrix(*h_ord);
let (g, b) = y_h[bus][bus];
let y_mag = (g * g + b * b).sqrt();
(v_mag * y_mag).powi(2)
})
.sum();
(sum_sq.sqrt() / i_fund) * 100.0
})
.collect();
let mut branch_harmonic_flows: Vec<Vec<(HarmonicOrder, f64)>> =
vec![Vec::new(); self.branches.len()];
for (br_idx, branch) in self.branches.iter().enumerate() {
for &h in &self.config.harmonic_orders {
let v_from = harmonic_v[branch.from]
.iter()
.find(|(order, _, _)| *order == h)
.map(|&(_, mag, ang)| (mag * ang.cos(), mag * ang.sin()))
.unwrap_or((0.0, 0.0));
let v_to = harmonic_v[branch.to]
.iter()
.find(|(order, _, _)| *order == h)
.map(|&(_, mag, ang)| (mag * ang.cos(), mag * ang.sin()))
.unwrap_or((0.0, 0.0));
let dv = cx_sub(v_from, v_to);
let (g_h, b_h) = branch.admittance_at_harmonic(h);
let i_branch = cx_mul(dv, (g_h, b_h));
let i_mag = cx_abs(i_branch);
branch_harmonic_flows[br_idx].push((h, i_mag));
}
}
let mut total_harmonic_losses_mw: f64 = 0.0;
for (br_idx, branch) in self.branches.iter().enumerate() {
for &(h, i_mag) in &branch_harmonic_flows[br_idx] {
let (r_h, _) = branch.impedance_at_harmonic(h);
total_harmonic_losses_mw += i_mag * i_mag * r_h * self.config.base_mva;
}
}
let ieee519_compliance: Vec<IeeComplianceResult> = (0..n)
.map(|bus| {
let v_thd_lim = Self::ieee519_voltage_limit(self.config.nominal_voltage_kv);
let i_thd_lim = Self::ieee519_current_limit(self.config.nominal_voltage_kv);
let v_thd_act = bus_thd_v[bus];
let i_thd_act = bus_thd_i[bus];
let compliant = v_thd_act <= v_thd_lim && i_thd_act <= i_thd_lim;
IeeComplianceResult {
bus,
voltage_thd_limit_pct: v_thd_lim,
voltage_thd_actual_pct: v_thd_act,
current_thd_limit_pct: i_thd_lim,
current_thd_actual_pct: i_thd_act,
compliant,
}
})
.collect();
Ok(HarmonicPfResultV1 {
fundamental_voltages: v_fund_mag,
fundamental_angles: v_fund_ang,
harmonic_voltages: harmonic_v,
bus_thd_v,
bus_thd_i,
branch_harmonic_flows,
total_harmonic_losses_mw,
ieee519_compliance,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::f64::consts::PI;
fn two_bus_system() -> HarmonicPowerFlow {
let branches = vec![HarmonicBranch::new(0, 1, 0.01, 0.1, 0.02, 100.0)];
let sources = vec![HarmonicSource::new(
1,
HarmonicSourceType::SixPulseDrive,
0.5,
)];
let loads = vec![HarmonicLoad {
bus: 1,
p_fundamental_mw: 10.0,
q_fundamental_mvar: 3.0,
load_type: HarmonicLoadType::LinearResistive,
}];
let config = HarmonicPfConfigV1 {
base_mva: 100.0,
fundamental_hz: 50.0,
harmonic_orders: vec![3, 5, 7, 11, 13],
max_iterations: 100,
tolerance_pu: 1e-8,
nominal_voltage_kv: 20.0,
};
HarmonicPowerFlow::new(2, branches, sources, loads, config, 0)
}
#[test]
fn test_harmonic_source_six_pulse_spectrum() {
let spectrum = HarmonicSourceType::SixPulseDrive.spectrum(1.0);
let fifth = spectrum
.iter()
.find(|(h, _, _)| *h == 5)
.expect("5th harmonic");
assert!((fifth.1 - 0.175).abs() < 1e-9, "5th should be 17.5%");
let seventh = spectrum
.iter()
.find(|(h, _, _)| *h == 7)
.expect("7th harmonic");
assert!((seventh.1 - 0.111).abs() < 1e-9, "7th should be 11.1%");
let has_even = spectrum.iter().any(|(h, _, _)| h % 2 == 0);
assert!(!has_even, "6-pulse drive should have no even harmonics");
}
#[test]
fn test_harmonic_source_twelve_pulse_spectrum() {
let spectrum = HarmonicSourceType::TwelvePulseDrive.spectrum(1.0);
let eleventh = spectrum.iter().find(|(h, _, _)| *h == 11).expect("11th");
assert!((eleventh.1 - 0.076).abs() < 1e-9, "11th should be 7.6%");
let thirteenth = spectrum.iter().find(|(h, _, _)| *h == 13).expect("13th");
assert!((thirteenth.1 - 0.059).abs() < 1e-9, "13th should be 5.9%");
let fifth = spectrum.iter().find(|(h, _, _)| *h == 5);
assert!(fifth.is_none(), "12-pulse should cancel 5th harmonic");
let seventh = spectrum.iter().find(|(h, _, _)| *h == 7);
assert!(seventh.is_none(), "12-pulse should cancel 7th harmonic");
}
#[test]
fn test_harmonic_source_pv_spectrum() {
let spectrum = HarmonicSourceType::PhotovoltaicInverter.spectrum(1.0);
let fifth = spectrum.iter().find(|(h, _, _)| *h == 5).expect("5th");
assert!(fifth.1 <= 0.05, "PV 5th harmonic should be <= 5%");
let seventh = spectrum.iter().find(|(h, _, _)| *h == 7).expect("7th");
assert!(seventh.1 <= 0.04, "PV 7th harmonic should be <= 4%");
let third = spectrum.iter().find(|(h, _, _)| *h == 3);
assert!(third.is_none(), "PV inverter should not have 3rd harmonic");
}
#[test]
fn test_harmonic_source_smps_spectrum() {
let spectrum = HarmonicSourceType::SwitchModePowerSupply.spectrum(1.0);
let third = spectrum.iter().find(|(h, _, _)| *h == 3).expect("3rd");
assert!((third.1 - 0.85).abs() < 1e-9, "SMPS 3rd should be 85%");
let fifth = spectrum.iter().find(|(h, _, _)| *h == 5).expect("5th");
assert!((fifth.1 - 0.52).abs() < 1e-9, "SMPS 5th should be 52%");
let spectrum2 = HarmonicSourceType::SwitchModePowerSupply.spectrum(2.0);
let third2 = spectrum2.iter().find(|(h, _, _)| *h == 3).expect("3rd x2");
assert!(
(third2.1 - 1.70).abs() < 1e-9,
"scaling by 2 should double magnitudes"
);
}
#[test]
fn test_branch_impedance_at_5th_harmonic() {
let branch = HarmonicBranch::new(0, 1, 0.01, 0.1, 0.0, 100.0);
let (r5, x5) = branch.impedance_at_harmonic(5);
let r5_expected = 0.01 * (5.0_f64).sqrt();
assert!(
(r5 - r5_expected).abs() < 1e-10,
"R at 5th: {r5} vs {r5_expected}"
);
assert!((x5 - 0.5).abs() < 1e-10, "X at 5th: {x5}");
}
#[test]
fn test_branch_impedance_at_11th_harmonic() {
let branch = HarmonicBranch::new(0, 1, 0.01, 0.1, 0.0, 100.0);
let (r11, x11) = branch.impedance_at_harmonic(11);
let r11_expected = 0.01 * (11.0_f64).sqrt();
assert!(
(r11 - r11_expected).abs() < 1e-10,
"R at 11th: {r11} vs {r11_expected}"
);
assert!((x11 - 1.1).abs() < 1e-10, "X at 11th: {x11}");
}
#[test]
fn test_branch_admittance_at_harmonic() {
let branch = HarmonicBranch::new(0, 1, 0.01, 0.1, 0.0, 100.0);
let (g, b) = branch.admittance_at_harmonic(5);
let (r5, x5) = branch.impedance_at_harmonic(5);
let denom = r5 * r5 + x5 * x5;
let g_exp = r5 / denom;
let b_exp = -x5 / denom;
assert!((g - g_exp).abs() < 1e-12, "G at 5th");
assert!((b - b_exp).abs() < 1e-12, "B at 5th");
assert!(g > 0.0);
assert!(b < 0.0);
}
#[test]
fn test_build_admittance_matrix_2bus() {
let hpf = two_bus_system();
let y = hpf.build_admittance_matrix(1);
assert_eq!(y.len(), 2);
assert_eq!(y[0].len(), 2);
let (g00, _b00) = y[0][0];
let (g01, b01) = y[0][1];
assert!(
g01 < 0.0 || b01 < 0.0,
"off-diagonal should be negative of series admittance"
);
let (g10, _b10) = y[1][0];
assert!((g00 - g00.abs()).abs() < 1e-10, "G00 should be positive");
let _ = g10;
}
#[test]
fn test_build_admittance_matrix_3bus() {
let branches = vec![
HarmonicBranch::new(0, 1, 0.01, 0.1, 0.01, 100.0),
HarmonicBranch::new(1, 2, 0.02, 0.15, 0.01, 100.0),
HarmonicBranch::new(0, 2, 0.015, 0.12, 0.01, 100.0),
];
let config = HarmonicPfConfigV1::default();
let hpf = HarmonicPowerFlow::new(3, branches, vec![], vec![], config, 0);
let y = hpf.build_admittance_matrix(1);
assert_eq!(y.len(), 3);
for (i, y_row) in y.iter().enumerate() {
for (j, &(gij, bij)) in y_row.iter().enumerate() {
let (gji, bji) = y[j][i];
assert!((gij - gji).abs() < 1e-12, "G[{i}][{j}] != G[{j}][{i}]");
assert!((bij - bji).abs() < 1e-12, "B[{i}][{j}] != B[{j}][{i}]");
}
}
for (i, y_row) in y.iter().enumerate() {
let (gii, _) = y_row[i];
assert!(gii > 0.0, "G[{i}][{i}] should be positive");
}
}
#[test]
fn test_harmonic_injection_at_bus() {
let hpf = two_bus_system();
let (ir5, ii5) = hpf.harmonic_injection(1, 5);
let mag = (ir5 * ir5 + ii5 * ii5).sqrt();
assert!(
(mag - 0.5 * 0.175).abs() < 1e-10,
"5th harmonic injection magnitude"
);
let (ir0, ii0) = hpf.harmonic_injection(0, 5);
assert!(
(ir0 * ir0 + ii0 * ii0).sqrt() < 1e-10,
"No injection at slack bus"
);
}
#[test]
fn test_solve_harmonic_2bus() {
let hpf = two_bus_system();
let y = hpf.build_admittance_matrix(5);
let i_h: Vec<Cx> = vec![(0.0, 0.0), hpf.harmonic_injection(1, 5)];
let v_h = hpf.solve_harmonic(5, &y, &i_h).expect("harmonic solve ok");
assert_eq!(v_h.len(), 2);
let v_slack = cx_abs(v_h[0]);
assert!(
v_slack < 1e-10,
"Slack bus harmonic voltage should be ~0, got {v_slack}"
);
let v1 = cx_abs(v_h[1]);
assert!(
v1 > 1e-6,
"Bus 1 should have non-zero harmonic voltage {v1}"
);
}
#[test]
fn test_solve_fundamental_2bus() {
let hpf = two_bus_system();
let p_inj = vec![0.0, -0.1]; let q_inj = vec![0.0, -0.03];
let (v_mag, v_ang) = hpf
.solve_fundamental(&p_inj, &q_inj)
.expect("fundamental ok");
assert_eq!(v_mag.len(), 2);
assert!((v_mag[0] - 1.0).abs() < 1e-6, "Slack voltage: {}", v_mag[0]);
assert!(
v_mag[1] < 1.01,
"Load bus voltage should be <= 1.01: {}",
v_mag[1]
);
assert!(
v_mag[1] > 0.9,
"Load bus voltage should be > 0.9: {}",
v_mag[1]
);
assert!(v_ang[0].abs() < 1e-10, "Slack angle: {}", v_ang[0]);
}
#[test]
fn test_compute_voltage_thd() {
let harmonics = vec![(5u32, 0.05_f64, 0.0_f64), (7u32, 0.03, 0.0)];
let thd = HarmonicPowerFlow::compute_voltage_thd(1.0, &harmonics);
let expected = (0.0025_f64 + 0.0009_f64).sqrt() * 100.0;
assert!((thd - expected).abs() < 1e-8, "THD: {thd} vs {expected}");
}
#[test]
fn test_thd_pure_fundamental() {
let harmonics: Vec<(HarmonicOrder, f64, f64)> = vec![];
let thd = HarmonicPowerFlow::compute_voltage_thd(1.0, &harmonics);
assert!(
thd.abs() < 1e-12,
"Pure fundamental should have THD = 0, got {thd}"
);
let harmonics_fund_only = vec![(1u32, 1.0_f64, 0.0_f64)];
let thd2 = HarmonicPowerFlow::compute_voltage_thd(1.0, &harmonics_fund_only);
assert!(
thd2.abs() < 1e-12,
"Only fundamental should give THD = 0, got {thd2}"
);
}
#[test]
fn test_ieee519_voltage_limit_lv() {
let limit = HarmonicPowerFlow::ieee519_voltage_limit(0.4);
assert!(
(limit - 8.0).abs() < 1e-10,
"LV limit should be 8%: {limit}"
);
let limit_at1 = HarmonicPowerFlow::ieee519_voltage_limit(1.0);
assert!(
(limit_at1 - 8.0).abs() < 1e-10,
"1 kV boundary: {limit_at1}"
);
}
#[test]
fn test_ieee519_voltage_limit_mv() {
let limit = HarmonicPowerFlow::ieee519_voltage_limit(20.0);
assert!(
(limit - 5.0).abs() < 1e-10,
"20 kV (MV) limit should be 5%: {limit}"
);
let limit_hv_edge = HarmonicPowerFlow::ieee519_voltage_limit(69.0);
assert!(
(limit_hv_edge - 5.0).abs() < 1e-10,
"69 kV boundary: {limit_hv_edge}"
);
}
#[test]
fn test_ieee519_voltage_limit_hv() {
let limit = HarmonicPowerFlow::ieee519_voltage_limit(110.0);
assert!(
(limit - 2.5).abs() < 1e-10,
"110 kV (HV) limit should be 2.5%: {limit}"
);
let limit_ehv = HarmonicPowerFlow::ieee519_voltage_limit(400.0);
assert!(
(limit_ehv - 1.5).abs() < 1e-10,
"400 kV (EHV) limit should be 1.5%: {limit_ehv}"
);
let limit_161 = HarmonicPowerFlow::ieee519_voltage_limit(161.0);
assert!(
(limit_161 - 2.5).abs() < 1e-10,
"161 kV boundary: {limit_161}"
);
}
#[test]
fn test_find_resonance_frequencies() {
let branches = vec![HarmonicBranch {
from: 0,
to: 1,
r_fundamental_pu: 0.0005, x_fundamental_pu: 0.04,
b_fundamental_pu: 2.0, r_skin_effect_exp: 0.5,
rating_mva: 100.0,
}];
let config = HarmonicPfConfigV1 {
harmonic_orders: vec![5, 7, 11, 13],
..Default::default()
};
let hpf = HarmonicPowerFlow::new(2, branches, vec![], vec![], config, 0);
let resonances = hpf.find_resonance_frequencies(0);
let near_5th = resonances.iter().any(|&h| (4..=6).contains(&h));
assert!(
near_5th,
"Should detect resonance near 5th harmonic, got: {resonances:?}"
);
}
#[test]
fn test_design_passive_filter_5th() {
let (l, c) = HarmonicPowerFlow::design_passive_filter(5, 20.0, 5.0, 50.0);
let omega1 = 2.0 * PI * 50.0;
let c_expected = 5e6 / (20_000.0_f64.powi(2) * omega1);
assert!(
(c - c_expected).abs() < 1e-18,
"Capacitance: {c} vs {c_expected}"
);
let omega5 = 5.0 * omega1;
let l_expected = 1.0 / (c_expected * omega5 * omega5);
assert!(
(l - l_expected).abs() < 1e-18,
"Inductance: {l} vs {l_expected}"
);
assert!(l > 0.0, "L should be positive: {l}");
assert!(c > 0.0, "C should be positive: {c}");
let f_tuned = 1.0 / (2.0 * PI * (l * c).sqrt());
let f_5th = 5.0 * 50.0;
assert!(
(f_tuned - f_5th).abs() / f_5th < 1e-9,
"Filter tuned at {f_tuned} Hz, expected {f_5th} Hz"
);
}
#[test]
fn test_full_harmonic_pf_2bus() {
let hpf = two_bus_system();
let p_inj = vec![0.0, -0.1]; let q_inj = vec![0.0, -0.03];
let result = hpf
.solve(&p_inj, &q_inj)
.expect("harmonic PF should converge");
assert_eq!(result.fundamental_voltages.len(), 2);
assert!(
(result.fundamental_voltages[0] - 1.0).abs() < 1e-4,
"Slack voltage: {}",
result.fundamental_voltages[0]
);
assert_eq!(result.harmonic_voltages.len(), 2);
assert_eq!(
result.harmonic_voltages[1].len(),
5,
"bus 1 should have 5 harmonic entries"
);
for (i, &thd) in result.bus_thd_v.iter().enumerate() {
assert!(thd >= 0.0, "Bus {i} THD_V should be >= 0: {thd}");
}
assert_eq!(result.branch_harmonic_flows.len(), 1, "One branch");
assert_eq!(
result.branch_harmonic_flows[0].len(),
5,
"5 harmonic orders per branch"
);
assert!(
result.total_harmonic_losses_mw >= 0.0,
"Harmonic losses must be >= 0: {}",
result.total_harmonic_losses_mw
);
assert_eq!(result.ieee519_compliance.len(), 2);
assert!((result.ieee519_compliance[0].voltage_thd_limit_pct - 5.0).abs() < 1e-9);
let thd_bus1 = result.bus_thd_v[1];
assert!(
thd_bus1 > 0.0,
"Bus 1 with harmonic source should have THD > 0: {thd_bus1}"
);
}
#[test]
fn test_solve_returns_ok_with_no_harmonic_sources() {
let branches = vec![
HarmonicBranch::new(0, 1, 0.01, 0.1, 0.0, 100.0),
HarmonicBranch::new(1, 2, 0.02, 0.15, 0.0, 100.0),
];
let loads = vec![
HarmonicLoad {
bus: 1,
p_fundamental_mw: 5.0,
q_fundamental_mvar: 1.0,
load_type: HarmonicLoadType::LinearResistive,
},
HarmonicLoad {
bus: 2,
p_fundamental_mw: 8.0,
q_fundamental_mvar: 2.0,
load_type: HarmonicLoadType::LinearResistive,
},
];
let config = HarmonicPfConfigV1 {
harmonic_orders: vec![5, 7],
..Default::default()
};
let hpf = HarmonicPowerFlow::new(3, branches, vec![], loads, config, 0);
let p_inj = vec![0.13, -0.05, -0.08];
let q_inj = vec![0.03, -0.01, -0.02];
let result = hpf
.solve(&p_inj, &q_inj)
.expect("solve must succeed with no harmonic sources");
assert_eq!(
result.fundamental_voltages.len(),
3,
"must have 3 bus voltages"
);
for (i, &v) in result.fundamental_voltages.iter().enumerate() {
assert!(
v.is_finite() && v > 0.0,
"bus {i} fundamental voltage must be finite and positive, got {v}"
);
}
}
#[test]
fn test_harmonic_injection_at_non_slack_bus() {
let branches = vec![HarmonicBranch::new(0, 1, 0.01, 0.05, 0.0, 100.0)];
let src = HarmonicSource::with_explicit_currents(
1,
HarmonicSourceType::SixPulseDrive,
vec![(5u32, 0.1, 0.0)],
);
let config = HarmonicPfConfigV1 {
harmonic_orders: vec![5],
..Default::default()
};
let hpf = HarmonicPowerFlow::new(2, branches, vec![src], vec![], config, 0);
let (ir1, ii1) = hpf.harmonic_injection(1, 5);
assert!(
(ir1 - 0.1).abs() < 1e-12,
"bus 1 h=5 real injection should be 0.1, got {ir1}"
);
assert!(
ii1.abs() < 1e-12,
"bus 1 h=5 imag injection should be 0.0, got {ii1}"
);
let (ir0, ii0) = hpf.harmonic_injection(0, 5);
assert!(
ir0.abs() < 1e-12 && ii0.abs() < 1e-12,
"slack bus injection should be zero, got ({ir0}, {ii0})"
);
}
#[test]
fn test_thd_from_harmonic_solution() {
let hpf = two_bus_system();
let p_inj = vec![0.0, -0.1];
let q_inj = vec![0.0, -0.03];
let result = hpf
.solve(&p_inj, &q_inj)
.expect("two_bus_system solve must succeed");
let v1 = result.fundamental_voltages[1];
let thd_manual = HarmonicPowerFlow::compute_voltage_thd(v1, &result.harmonic_voltages[1]);
let thd_stored = result.bus_thd_v[1];
assert!(
(thd_manual - thd_stored).abs() < 1e-6,
"manually computed THD {thd_manual} should match stored {thd_stored}"
);
assert!(thd_manual >= 0.0, "THD must be non-negative: {thd_manual}");
}
#[test]
fn test_harmonic_voltages_finite_and_positive() {
let hpf = two_bus_system();
let p_inj = vec![0.0, -0.05];
let q_inj = vec![0.0, -0.01];
let result = hpf
.solve(&p_inj, &q_inj)
.expect("two_bus_system solve must succeed");
for bus in 0..2 {
for &(order, mag, _angle) in &result.harmonic_voltages[bus] {
assert!(
mag.is_finite(),
"bus {bus} h={order} magnitude must be finite, got {mag}"
);
assert!(
mag >= 0.0,
"bus {bus} h={order} magnitude must be >= 0, got {mag}"
);
}
}
}
#[test]
fn test_multiple_harmonic_orders_3_5_7() {
let branches = vec![HarmonicBranch::new(0, 1, 0.01, 0.1, 0.0, 100.0)];
let sources = vec![HarmonicSource::new(
1,
HarmonicSourceType::SwitchModePowerSupply,
0.3,
)];
let config = HarmonicPfConfigV1 {
harmonic_orders: vec![3, 5, 7],
..Default::default()
};
let hpf = HarmonicPowerFlow::new(2, branches, sources, vec![], config, 0);
let p_inj = vec![0.0, -0.05];
let q_inj = vec![0.0, -0.01];
let result = hpf
.solve(&p_inj, &q_inj)
.expect("solve with 3 harmonic orders must succeed");
assert_eq!(
result.harmonic_voltages[1].len(),
3,
"bus 1 should have exactly 3 harmonic entries (orders 3, 5, 7)"
);
let orders: Vec<u32> = result.harmonic_voltages[1]
.iter()
.map(|&(h, _, _)| h)
.collect();
for &expected_order in &[3u32, 5, 7] {
assert!(
orders.contains(&expected_order),
"bus 1 harmonic voltages should contain order {expected_order}, got {orders:?}"
);
}
}
#[test]
fn test_norton_source_injection_angle() {
let src = HarmonicSource::with_explicit_currents(
0,
HarmonicSourceType::SixPulseDrive,
vec![(5u32, 1.0, PI / 4.0)],
);
let (ir, ii) = src.injection_at(5);
let expected_r = (PI / 4.0).cos();
let expected_i = (PI / 4.0).sin();
assert!(
(ir - expected_r).abs() < 1e-10,
"real part at h=5 should be cos(PI/4)={expected_r}, got {ir}"
);
assert!(
(ii - expected_i).abs() < 1e-10,
"imag part at h=5 should be sin(PI/4)={expected_i}, got {ii}"
);
let (ir7, ii7) = src.injection_at(7);
assert!(
ir7.abs() < 1e-12 && ii7.abs() < 1e-12,
"injection at h=7 (not listed) should be zero, got ({ir7}, {ii7})"
);
}
#[test]
fn test_shunt_susceptance_scales_linearly() {
let branch = HarmonicBranch::new(0, 1, 0.01, 0.1, 0.05, 100.0);
let b1 = branch.shunt_susceptance_at_harmonic(1);
assert!(
(b1 - 0.05).abs() < 1e-12,
"h=1 shunt susceptance should be 0.05, got {b1}"
);
let b5 = branch.shunt_susceptance_at_harmonic(5);
assert!(
(b5 - 0.25).abs() < 1e-12,
"h=5 shunt susceptance should be 0.25, got {b5}"
);
let b7 = branch.shunt_susceptance_at_harmonic(7);
assert!(
(b7 - 0.35).abs() < 1e-12,
"h=7 shunt susceptance should be 0.35, got {b7}"
);
}
#[test]
fn test_zero_injection_produces_near_zero_harmonic_voltages() {
let branches = vec![HarmonicBranch::new(0, 1, 0.01, 0.1, 0.0, 100.0)];
let loads = vec![HarmonicLoad {
bus: 1,
p_fundamental_mw: 5.0,
q_fundamental_mvar: 1.0,
load_type: HarmonicLoadType::LinearResistive,
}];
let config = HarmonicPfConfigV1 {
harmonic_orders: vec![5, 7],
..Default::default()
};
let hpf = HarmonicPowerFlow::new(2, branches, vec![], loads, config, 0);
let result = hpf
.solve(&[0.0, 0.0], &[0.0, 0.0])
.expect("solve with zero injections must succeed");
for &(order, mag, _angle) in &result.harmonic_voltages[1] {
assert!(
mag < 1e-10,
"bus 1 h={order} voltage must be near-zero with no harmonic injection, got {mag}"
);
}
}
}