use crate::error::{OxiGridError, Result};
use crate::network::topology::PowerNetwork;
use num_complex::Complex64;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone)]
pub struct HarmonicPowerFlow {
pub fundamental_hz: f64,
pub harmonics: Vec<usize>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HarmonicInjection {
pub bus: usize,
pub harmonic_order: usize,
pub current_magnitude_pu: f64,
pub current_angle_rad: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HarmonicPfResult {
pub harmonic_voltages: Vec<Vec<Complex64>>,
pub thd_per_bus: Vec<f64>,
pub individual_harmonics: Vec<Vec<f64>>,
pub voltage_harmonic_spectrum: Vec<Vec<f64>>,
}
impl HarmonicPowerFlow {
pub fn new(fundamental_hz: f64, harmonics: Vec<usize>) -> Self {
Self {
fundamental_hz,
harmonics,
}
}
pub fn solve(
&self,
network: &PowerNetwork,
injections: &[HarmonicInjection],
) -> Result<HarmonicPfResult> {
let n_bus = network.bus_count();
let n_harm = self.harmonics.len();
if n_bus == 0 {
return Err(OxiGridError::InvalidNetwork(
"Network has no buses".to_string(),
));
}
for inj in injections {
if inj.bus >= n_bus {
return Err(OxiGridError::InvalidParameter(format!(
"Injection bus index {} out of range (n_bus={n_bus})",
inj.bus
)));
}
if !self.harmonics.contains(&inj.harmonic_order) {
return Err(OxiGridError::InvalidParameter(format!(
"Harmonic order {} not in harmonics list {:?}",
inj.harmonic_order, self.harmonics
)));
}
}
let mut harmonic_voltages: Vec<Vec<Complex64>> =
vec![vec![Complex64::new(0.0, 0.0); n_harm]; n_bus];
for (h_idx, &h) in self.harmonics.iter().enumerate() {
let y_h = build_harmonic_y_dense(network, h)?;
let mut i_h: Vec<Complex64> = vec![Complex64::new(0.0, 0.0); n_bus];
for inj in injections {
if inj.harmonic_order == h {
let i_phasor =
Complex64::from_polar(inj.current_magnitude_pu, inj.current_angle_rad);
i_h[inj.bus] += i_phasor;
}
}
let v_h = dense_solve(&y_h, &i_h, n_bus)?;
for bus in 0..n_bus {
harmonic_voltages[bus][h_idx] = v_h[bus];
}
}
let v1_assumed = 1.0_f64;
let mut thd_per_bus = vec![0.0_f64; n_bus];
let mut individual_harmonics = vec![vec![0.0_f64; n_harm]; n_bus];
let mut voltage_harmonic_spectrum = vec![vec![0.0_f64; n_harm]; n_bus];
for bus in 0..n_bus {
let mut sum_sq = 0.0_f64;
for h_idx in 0..n_harm {
let v_mag = harmonic_voltages[bus][h_idx].norm();
voltage_harmonic_spectrum[bus][h_idx] = v_mag;
let ihd = 100.0 * v_mag / v1_assumed;
individual_harmonics[bus][h_idx] = ihd;
sum_sq += v_mag * v_mag;
}
thd_per_bus[bus] = 100.0 * sum_sq.sqrt() / v1_assumed;
}
Ok(HarmonicPfResult {
harmonic_voltages,
thd_per_bus,
individual_harmonics,
voltage_harmonic_spectrum,
})
}
}
fn build_harmonic_y_dense(network: &PowerNetwork, h: usize) -> Result<Vec<Complex64>> {
let n = network.bus_count();
let mut y = vec![Complex64::new(0.0, 0.0); n * n];
let hf = h as f64;
for branch in &network.branches {
if !branch.status {
continue;
}
let i = network.bus_index(branch.from_bus)?;
let j = network.bus_index(branch.to_bus)?;
let z_h = Complex64::new(branch.r, hf * branch.x);
if z_h.norm_sqr() < 1e-30 {
continue; }
let y_series = z_h.inv();
let b_shunt = hf * branch.b / 2.0;
let y_shunt = Complex64::new(0.0, b_shunt);
let tap = branch.tap_complex();
let tap_conj = tap.conj();
let tap_sq = tap.norm_sqr();
let yii = y_series / tap_sq + y_shunt;
let yjj = y_series + y_shunt;
let yij = -y_series / tap_conj;
let yji = -y_series / tap;
y[i * n + i] += yii;
y[j * n + j] += yjj;
y[i * n + j] += yij;
y[j * n + i] += yji;
}
for (k, bus) in network.buses.iter().enumerate() {
if bus.gs != 0.0 || bus.bs != 0.0 {
let y_shunt = Complex64::new(bus.gs, hf * bus.bs) / network.base_mva;
y[k * n + k] += y_shunt;
}
}
Ok(y)
}
fn dense_solve(a_flat: &[Complex64], b: &[Complex64], n: usize) -> Result<Vec<Complex64>> {
let mut mat: Vec<Vec<Complex64>> = (0..n)
.map(|i| {
let mut row: Vec<Complex64> = a_flat[i * n..(i + 1) * n].to_vec();
row.push(b[i]);
row
})
.collect();
for col in 0..n {
let pivot_row = (col..n)
.max_by(|&r1, &r2| {
mat[r1][col]
.norm()
.partial_cmp(&mat[r2][col].norm())
.unwrap_or(std::cmp::Ordering::Equal)
})
.unwrap_or(col);
if mat[pivot_row][col].norm() < 1e-30 {
return Err(OxiGridError::LinearAlgebra(format!(
"Harmonic Y-bus is singular at column {col} — check network connectivity"
)));
}
mat.swap(col, pivot_row);
let pivot = mat[col][col];
let ncols = n + 1;
{
let row_copy: Vec<_> = mat[col][col..ncols].to_vec();
for (ci, v) in row_copy.iter().enumerate() {
mat[col][col + ci] = *v / pivot;
}
}
for row in 0..n {
if row == col {
continue;
}
let factor = mat[row][col];
if factor.norm() < 1e-30 {
continue;
}
let pivot_row_slice: Vec<_> = mat[col][col..ncols].to_vec();
for (ci, &pv) in pivot_row_slice.iter().enumerate() {
mat[row][col + ci] -= factor * pv;
}
}
}
Ok((0..n).map(|i| mat[i][n]).collect())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::network::branch::Branch;
use crate::network::bus::{Bus, BusType};
use crate::network::topology::{Generator, PowerNetwork};
use std::f64::consts::PI;
fn make_two_bus_network() -> PowerNetwork {
let mut net = PowerNetwork::new(100.0);
net.buses.push(Bus::new(1, BusType::Slack));
net.buses.push(Bus::new(2, BusType::PQ));
net.branches.push(Branch {
from_bus: 1,
to_bus: 2,
r: 0.01,
x: 0.1,
b: 0.01,
rate_a: 100.0,
rate_b: 0.0,
rate_c: 0.0,
tap: 0.0,
shift: 0.0,
status: true,
});
net.generators.push(Generator {
bus_id: 1,
pg: 0.5,
qg: 0.0,
qmax: 100.0,
qmin: -100.0,
vg: 1.0,
mbase: 100.0,
status: true,
pmax: 100.0,
pmin: 0.0,
});
net
}
#[test]
fn test_harmonic_pf_result_dimensions() {
let net = make_two_bus_network();
let hpf = HarmonicPowerFlow::new(60.0, vec![3, 5, 7]);
let injections = vec![HarmonicInjection {
bus: 1,
harmonic_order: 5,
current_magnitude_pu: 0.05,
current_angle_rad: 0.0,
}];
let result = hpf.solve(&net, &injections).expect("should succeed");
assert_eq!(result.harmonic_voltages.len(), 2, "one entry per bus");
assert_eq!(
result.harmonic_voltages[0].len(),
3,
"one entry per harmonic"
);
assert_eq!(result.thd_per_bus.len(), 2);
assert_eq!(result.individual_harmonics.len(), 2);
assert_eq!(result.voltage_harmonic_spectrum.len(), 2);
}
#[test]
fn test_harmonic_pf_zero_injection_zero_voltage() {
let net = make_two_bus_network();
let hpf = HarmonicPowerFlow::new(50.0, vec![3, 5, 7, 11, 13]);
let result = hpf.solve(&net, &[]).expect("should succeed");
for (bus, row) in result.harmonic_voltages.iter().enumerate() {
for (h_idx, v) in row.iter().enumerate() {
assert!(
v.norm() < 1e-10,
"bus={bus} h_idx={h_idx}: expected 0 voltage, got |V|={:.3e}",
v.norm()
);
}
}
for (i, &thd) in result.thd_per_bus.iter().enumerate() {
assert!(
thd < 1e-8,
"bus {i}: THD={thd:.4e} should be ~0 with no injections"
);
}
}
#[test]
fn test_harmonic_pf_nonzero_injection_nonzero_voltage() {
let net = make_two_bus_network();
let hpf = HarmonicPowerFlow::new(60.0, vec![5]);
let injections = vec![HarmonicInjection {
bus: 1,
harmonic_order: 5,
current_magnitude_pu: 0.1,
current_angle_rad: PI / 6.0,
}];
let result = hpf.solve(&net, &injections).expect("should succeed");
let v5_bus1 = result.harmonic_voltages[1][0].norm();
assert!(
v5_bus1 > 1e-8,
"5th harmonic voltage at bus 1 should be non-zero, got {v5_bus1:.3e}"
);
assert!(
result.thd_per_bus[1] > 0.0,
"THD at bus 1 should be positive"
);
}
#[test]
fn test_harmonic_pf_invalid_bus_index_errors() {
let net = make_two_bus_network();
let hpf = HarmonicPowerFlow::new(50.0, vec![3]);
let injections = vec![HarmonicInjection {
bus: 99, harmonic_order: 3,
current_magnitude_pu: 0.05,
current_angle_rad: 0.0,
}];
assert!(hpf.solve(&net, &injections).is_err());
}
#[test]
fn test_harmonic_pf_higher_order_larger_impedance() {
let net = make_two_bus_network();
let inj_magnitude = 0.05;
let inj_angle = 0.0;
let hpf3 = HarmonicPowerFlow::new(50.0, vec![3]);
let inj3 = vec![HarmonicInjection {
bus: 1,
harmonic_order: 3,
current_magnitude_pu: inj_magnitude,
current_angle_rad: inj_angle,
}];
let res3 = hpf3.solve(&net, &inj3).expect("h=3 solve");
let hpf13 = HarmonicPowerFlow::new(50.0, vec![13]);
let inj13 = vec![HarmonicInjection {
bus: 1,
harmonic_order: 13,
current_magnitude_pu: inj_magnitude,
current_angle_rad: inj_angle,
}];
let res13 = hpf13.solve(&net, &inj13).expect("h=13 solve");
let v3 = res3.harmonic_voltages[1][0].norm();
let v13 = res13.harmonic_voltages[1][0].norm();
let rel_diff = (v3 - v13).abs() / (v3 + v13 + 1e-15);
assert!(
rel_diff > 1e-3,
"3rd and 13th harmonic voltages should differ (v3={v3:.4e}, v13={v13:.4e})"
);
}
#[test]
fn test_harmonic_pf_invalid_harmonic_order_errors() {
let net = make_two_bus_network();
let hpf = HarmonicPowerFlow::new(50.0, vec![3, 5, 7]);
let injections = vec![HarmonicInjection {
bus: 1,
harmonic_order: 9, current_magnitude_pu: 0.05,
current_angle_rad: 0.0,
}];
assert!(
hpf.solve(&net, &injections).is_err(),
"harmonic_order 9 not in [3,5,7] must return Err"
);
}
#[test]
fn test_harmonic_pf_multiple_injections_same_bus_sum() {
let net = make_two_bus_network();
let hpf = HarmonicPowerFlow::new(50.0, vec![5]);
let single_inj = vec![HarmonicInjection {
bus: 1,
harmonic_order: 5,
current_magnitude_pu: 0.05,
current_angle_rad: 0.0,
}];
let res_single = hpf
.solve(&net, &single_inj)
.expect("single injection solve");
let double_inj = vec![
HarmonicInjection {
bus: 1,
harmonic_order: 5,
current_magnitude_pu: 0.05,
current_angle_rad: 0.0,
},
HarmonicInjection {
bus: 1,
harmonic_order: 5,
current_magnitude_pu: 0.05,
current_angle_rad: 0.0,
},
];
let res_double = hpf
.solve(&net, &double_inj)
.expect("double injection solve");
let v_single = res_single.harmonic_voltages[1][0].norm();
let v_double = res_double.harmonic_voltages[1][0].norm();
assert!(
v_single > 1e-10,
"single injection must produce non-zero voltage, got {v_single:.3e}"
);
let ratio = v_double / v_single;
assert!(
(ratio - 2.0).abs() < 0.01,
"two identical injections at the same bus should double the voltage (ratio={ratio:.6}, expected 2.0)"
);
}
#[test]
fn test_individual_harmonics_equals_100x_spectrum() {
let net = make_two_bus_network();
let hpf = HarmonicPowerFlow::new(60.0, vec![5, 7]);
let injections = vec![HarmonicInjection {
bus: 1,
harmonic_order: 5,
current_magnitude_pu: 0.08,
current_angle_rad: 0.0,
}];
let result = hpf.solve(&net, &injections).expect("solve should succeed");
for (bus, (ind_row, spec_row)) in result
.individual_harmonics
.iter()
.zip(result.voltage_harmonic_spectrum.iter())
.enumerate()
{
for (h_idx, (&ind, &spec)) in ind_row.iter().zip(spec_row.iter()).enumerate() {
let expected = 100.0 * spec;
assert!(
(ind - expected).abs() < 1e-10,
"bus={bus} h_idx={h_idx}: individual_harmonics={ind:.10} != 100*spectrum={expected:.10}"
);
}
}
}
#[test]
fn test_harmonic_pf_non_standard_frequency_succeeds() {
let net = make_two_bus_network();
let hpf = HarmonicPowerFlow::new(400.0, vec![3, 5]);
let injections = vec![HarmonicInjection {
bus: 1,
harmonic_order: 3,
current_magnitude_pu: 0.05,
current_angle_rad: 0.0,
}];
let result = hpf
.solve(&net, &injections)
.expect("400 Hz solve should succeed");
assert_eq!(result.harmonic_voltages.len(), 2, "two buses");
assert_eq!(
result.harmonic_voltages[0].len(),
2,
"two harmonics in list"
);
}
#[test]
fn test_thd_non_negative() {
let net = make_two_bus_network();
let hpf = HarmonicPowerFlow::new(50.0, vec![3, 5, 7]);
let result = hpf
.solve(&net, &[])
.expect("zero-injection solve should succeed");
for (i, &thd) in result.thd_per_bus.iter().enumerate() {
assert!(
thd >= 0.0,
"bus {i}: thd_per_bus must be non-negative, got {thd}"
);
}
}
#[test]
fn test_injections_at_different_buses_both_produce_voltage() {
let net = make_two_bus_network();
let hpf = HarmonicPowerFlow::new(50.0, vec![3, 7]);
let injections = vec![
HarmonicInjection {
bus: 0,
harmonic_order: 3,
current_magnitude_pu: 0.05,
current_angle_rad: 0.0,
},
HarmonicInjection {
bus: 1,
harmonic_order: 7,
current_magnitude_pu: 0.05,
current_angle_rad: PI / 4.0,
},
];
let result = hpf
.solve(&net, &injections)
.expect("two-bus two-harmonic solve");
let v_bus0_h3 = result.harmonic_voltages[0][0].norm();
assert!(
v_bus0_h3 > 1e-10,
"bus 0, h=3: expected non-zero voltage, got {v_bus0_h3:.3e}"
);
let v_bus1_h7 = result.harmonic_voltages[1][1].norm();
assert!(
v_bus1_h7 > 1e-10,
"bus 1, h=7: expected non-zero voltage, got {v_bus1_h7:.3e}"
);
}
#[test]
fn test_harmonic_voltage_scales_linearly_with_current() {
let net = make_two_bus_network();
let hpf = HarmonicPowerFlow::new(50.0, vec![5]);
let inj_single = vec![HarmonicInjection {
bus: 1,
harmonic_order: 5,
current_magnitude_pu: 0.05,
current_angle_rad: 0.0,
}];
let res_single = hpf
.solve(&net, &inj_single)
.expect("single-magnitude solve");
let inj_double = vec![HarmonicInjection {
bus: 1,
harmonic_order: 5,
current_magnitude_pu: 0.10,
current_angle_rad: 0.0,
}];
let res_double = hpf
.solve(&net, &inj_double)
.expect("double-magnitude solve");
let v_single = res_single.harmonic_voltages[1][0].norm();
let v_double = res_double.harmonic_voltages[1][0].norm();
assert!(
v_single > 1e-10,
"single magnitude must produce non-zero voltage, got {v_single:.3e}"
);
let ratio = v_double / v_single;
assert!(
(ratio - 2.0).abs() < 0.01,
"doubling injection current must double harmonic voltage (ratio={ratio:.6}, expected 2.0)"
);
}
}