use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum EquivalentError {
#[error("bus index {0} out of range")]
BusOutOfRange(usize),
#[error("Y-bus inversion failed: singular or ill-conditioned")]
SingularYBus,
#[error("insufficient measurements for estimation")]
InsufficientMeasurements,
#[error("base_kv is zero at bus {0}")]
ZeroBaseKv(usize),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TheveninEquivalent {
pub bus: usize,
pub v_th_pu: f64,
pub theta_th_deg: f64,
pub z_th_ohm: (f64, f64),
pub z_th_pu: (f64, f64),
pub short_circuit_mva: f64,
pub short_circuit_ka: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NortonEquivalent {
pub bus: usize,
pub i_n_ka: f64,
pub theta_n_deg: f64,
pub y_n_siemens: (f64, f64),
pub z_n_pu: (f64, f64),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NetworkEquivalentExtractor {
pub base_mva: f64,
pub base_kv: Vec<f64>,
}
impl NetworkEquivalentExtractor {
pub fn new(base_mva: f64, base_kv: Vec<f64>) -> Self {
Self { base_mva, base_kv }
}
pub fn thevenin_at_bus(
&self,
bus: usize,
y_bus: &[Vec<(f64, f64)>],
v_oc_pu: f64,
theta_oc_deg: f64,
) -> Result<TheveninEquivalent, EquivalentError> {
let n = y_bus.len();
if bus >= n {
return Err(EquivalentError::BusOutOfRange(bus));
}
if bus >= self.base_kv.len() {
return Err(EquivalentError::BusOutOfRange(bus));
}
let bkv = self.base_kv[bus];
if bkv.abs() < 1e-12 {
return Err(EquivalentError::ZeroBaseKv(bus));
}
let (r_th_pu, x_th_pu) =
ybus_diagonal_impedance(y_bus, bus).ok_or(EquivalentError::SingularYBus)?;
let z_base = bkv * bkv / self.base_mva;
let r_th_ohm = r_th_pu * z_base;
let x_th_ohm = x_th_pu * z_base;
let z_th_mag_pu = (r_th_pu * r_th_pu + x_th_pu * x_th_pu).sqrt();
let sc_mva = if z_th_mag_pu > 1e-15 {
v_oc_pu * v_oc_pu / z_th_mag_pu * self.base_mva
} else {
f64::INFINITY
};
let sc_ka = sc_mva / (3.0_f64.sqrt() * bkv);
Ok(TheveninEquivalent {
bus,
v_th_pu: v_oc_pu,
theta_th_deg: theta_oc_deg,
z_th_ohm: (r_th_ohm, x_th_ohm),
z_th_pu: (r_th_pu, x_th_pu),
short_circuit_mva: sc_mva,
short_circuit_ka: sc_ka,
})
}
pub fn norton_at_bus(&self, thevenin: &TheveninEquivalent) -> NortonEquivalent {
let bus = thevenin.bus;
let (r, x) = thevenin.z_th_pu;
let z_mag2 = r * r + x * x;
let (g_n_pu, b_n_pu) = if z_mag2 > 1e-30 {
(r / z_mag2, -x / z_mag2)
} else {
(0.0, 0.0)
};
let bkv = if bus < self.base_kv.len() {
self.base_kv[bus]
} else {
1.0
};
let y_base = if bkv > 1e-12 {
self.base_mva / (bkv * bkv)
} else {
1.0
};
let g_n_s = g_n_pu * y_base;
let b_n_s = b_n_pu * y_base;
let i_n_ka = thevenin.short_circuit_ka;
let z_angle_deg = x.atan2(r).to_degrees();
let theta_n_deg = thevenin.theta_th_deg - z_angle_deg;
NortonEquivalent {
bus,
i_n_ka,
theta_n_deg,
y_n_siemens: (g_n_s, b_n_s),
z_n_pu: (r, x), }
}
pub fn thevenin_all_pq_buses(
&self,
y_bus: &[Vec<(f64, f64)>],
voltages: &[(f64, f64)],
pq_buses: &[usize],
) -> Result<Vec<TheveninEquivalent>, EquivalentError> {
let mut results = Vec::with_capacity(pq_buses.len());
for &bus in pq_buses {
if bus >= voltages.len() {
return Err(EquivalentError::BusOutOfRange(bus));
}
let (v_oc, theta_deg) = voltages[bus];
let th = self.thevenin_at_bus(bus, y_bus, v_oc, theta_deg)?;
results.push(th);
}
Ok(results)
}
pub fn estimate_from_measurements(
&self,
bus: usize,
op1: (f64, f64, f64, f64),
op2: (f64, f64, f64, f64),
base_kv: f64,
) -> Result<TheveninEquivalent, EquivalentError> {
if base_kv.abs() < 1e-12 {
return Err(EquivalentError::ZeroBaseKv(bus));
}
let (v1_mag, v1_ang_deg, p1_mw, q1_mvar) = op1;
let (v2_mag, v2_ang_deg, p2_mw, q2_mvar) = op2;
let v1 = polar_c(v1_mag, v1_ang_deg.to_radians());
let v2 = polar_c(v2_mag, v2_ang_deg.to_radians());
let s1 = CNum {
re: p1_mw / self.base_mva,
im: q1_mvar / self.base_mva,
};
let s2 = CNum {
re: p2_mw / self.base_mva,
im: q2_mvar / self.base_mva,
};
let i1 = conj_c(div_c(s1, v1));
let i2 = conj_c(div_c(s2, v2));
let dv = sub_c(v2, v1);
let di = sub_c(i2, i1);
let di_mag2 = di.re * di.re + di.im * di.im;
if di_mag2 < 1e-20 {
return Err(EquivalentError::InsufficientMeasurements);
}
let neg_dv = CNum {
re: -dv.re,
im: -dv.im,
};
let z_th = div_c(neg_dv, di);
let v_th = {
let z_i1 = mul_c(z_th, i1);
CNum {
re: v1.re + z_i1.re,
im: v1.im + z_i1.im,
}
};
let v_th_mag = (v_th.re * v_th.re + v_th.im * v_th.im).sqrt();
let v_th_ang_deg = v_th.im.atan2(v_th.re).to_degrees();
let z_base = base_kv * base_kv / self.base_mva;
let r_th_pu = z_th.re;
let x_th_pu = z_th.im;
let r_th_ohm = r_th_pu * z_base;
let x_th_ohm = x_th_pu * z_base;
let z_mag_pu = (r_th_pu * r_th_pu + x_th_pu * x_th_pu).sqrt();
let sc_mva = if z_mag_pu > 1e-15 {
v_th_mag * v_th_mag / z_mag_pu * self.base_mva
} else {
f64::INFINITY
};
let sc_ka = sc_mva / (3.0_f64.sqrt() * base_kv);
Ok(TheveninEquivalent {
bus,
v_th_pu: v_th_mag,
theta_th_deg: v_th_ang_deg,
z_th_ohm: (r_th_ohm, x_th_ohm),
z_th_pu: (r_th_pu, x_th_pu),
short_circuit_mva: sc_mva,
short_circuit_ka: sc_ka,
})
}
}
fn ybus_diagonal_impedance(y_bus: &[Vec<(f64, f64)>], bus: usize) -> Option<(f64, f64)> {
let n = y_bus.len();
let n2 = 2 * n;
let mut a = vec![vec![0.0f64; n2]; n2];
for i in 0..n {
for j in 0..n {
let (g, b) = y_bus[i][j];
a[i][j] = g; a[i][j + n] = -b; a[i + n][j] = b; a[i + n][j + n] = g; }
}
let mut b_vec = vec![0.0f64; n2];
b_vec[bus] = 1.0;
let x = gauss_solve(a, b_vec)?;
Some((x[bus], x[bus + n]))
}
#[allow(clippy::needless_range_loop)]
fn gauss_solve(mut a: Vec<Vec<f64>>, mut b: Vec<f64>) -> Option<Vec<f64>> {
let n = b.len();
for col in 0..n {
let mut max_val = a[col][col].abs();
let mut max_row = col;
for row in (col + 1)..n {
if a[row][col].abs() > max_val {
max_val = a[row][col].abs();
max_row = row;
}
}
if max_val < 1e-14 {
return None;
}
a.swap(col, max_row);
b.swap(col, max_row);
let pivot = a[col][col];
for row in (col + 1)..n {
let factor = a[row][col] / pivot;
for k in col..n {
let sub = factor * a[col][k];
a[row][k] -= sub;
}
b[row] -= factor * b[col];
}
}
let mut x = vec![0.0f64; n];
for i in (0..n).rev() {
x[i] = b[i];
for j in (i + 1)..n {
x[i] -= a[i][j] * x[j];
}
if a[i][i].abs() < 1e-30 {
return None;
}
x[i] /= a[i][i];
}
Some(x)
}
#[derive(Clone, Copy, Debug)]
struct CNum {
re: f64,
im: f64,
}
fn polar_c(mag: f64, ang: f64) -> CNum {
CNum {
re: mag * ang.cos(),
im: mag * ang.sin(),
}
}
fn sub_c(a: CNum, b: CNum) -> CNum {
CNum {
re: a.re - b.re,
im: a.im - b.im,
}
}
fn mul_c(a: CNum, b: CNum) -> CNum {
CNum {
re: a.re * b.re - a.im * b.im,
im: a.re * b.im + a.im * b.re,
}
}
fn div_c(a: CNum, b: CNum) -> CNum {
let d = b.re * b.re + b.im * b.im;
CNum {
re: (a.re * b.re + a.im * b.im) / d,
im: (a.im * b.re - a.re * b.im) / d,
}
}
fn conj_c(a: CNum) -> CNum {
CNum {
re: a.re,
im: -a.im,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[allow(dead_code)]
fn two_bus_ybus(r: f64, x: f64) -> Vec<Vec<(f64, f64)>> {
let z2 = r * r + x * x;
let g = r / z2;
let b = -x / z2;
vec![vec![(g, b), (-g, -b)], vec![(-g, -b), (g, b)]]
}
#[test]
fn test_two_bus_thevenin() {
let r = 0.01_f64;
let x = 0.1_f64;
let z2 = r * r + x * x;
let g = r / z2;
let b = -x / z2;
let shunt = 1e-4; let y_bus = vec![
vec![(g + shunt, b), (-g, -b)],
vec![(-g, -b), (g + shunt, b)],
];
let extractor = NetworkEquivalentExtractor::new(100.0, vec![110.0, 110.0]);
let result = extractor
.thevenin_at_bus(0, &y_bus, 1.0, 0.0)
.expect("thevenin_at_bus failed");
let (r_th, x_th) = result.z_th_pu;
assert!(r_th > 0.0, "R_th should be positive, got {r_th:.6}");
assert!(x_th > 0.0, "X_th should be positive, got {x_th:.6}");
assert_eq!(result.v_th_pu, 1.0);
assert_eq!(result.theta_th_deg, 0.0);
}
#[test]
fn test_thevenin_all_pq_buses() {
let z = (0.02_f64, 0.2_f64);
let (r, x) = z;
let z2 = r * r + x * x;
let g = r / z2;
let b = -x / z2;
let sh = 1e-4;
let y_bus = vec![
vec![(2.0 * g + sh, 2.0 * b), (-g, -b), (-g, -b)],
vec![(-g, -b), (2.0 * g + sh, 2.0 * b), (-g, -b)],
vec![(-g, -b), (-g, -b), (2.0 * g + sh, 2.0 * b)],
];
let voltages = vec![(1.0, 0.0), (0.99, -2.0), (0.98, -4.0)];
let pq_buses = vec![1, 2];
let extractor = NetworkEquivalentExtractor::new(100.0, vec![110.0, 110.0, 110.0]);
let results = extractor
.thevenin_all_pq_buses(&y_bus, &voltages, &pq_buses)
.expect("batch thevenin failed");
assert_eq!(results.len(), 2, "should return one result per PQ bus");
assert_eq!(results[0].bus, 1);
assert_eq!(results[1].bus, 2);
}
#[test]
fn test_norton_dual_consistency() {
let r = 0.01_f64;
let x = 0.1_f64;
let z2 = r * r + x * x;
let g = r / z2;
let b = -x / z2;
let sh = 1e-4;
let y_bus = vec![vec![(g + sh, b), (-g, -b)], vec![(-g, -b), (g + sh, b)]];
let extractor = NetworkEquivalentExtractor::new(100.0, vec![110.0, 110.0]);
let th = extractor
.thevenin_at_bus(0, &y_bus, 1.0, 5.0)
.expect("thevenin failed");
let nor = extractor.norton_at_bus(&th);
let (r_th, x_th) = th.z_th_pu;
let (r_n, x_n) = nor.z_n_pu;
assert!(
(r_n - r_th).abs() < 1e-10,
"R_N={r_n} should equal R_th={r_th}"
);
assert!(
(x_n - x_th).abs() < 1e-10,
"X_N={x_n} should equal X_th={x_th}"
);
}
#[test]
fn test_estimate_from_measurements() {
let r_th = 0.01_f64;
let x_th = 0.1_f64;
let v_th_re = 1.05_f64;
let v_th_im = 0.0_f64;
let z_mag2 = r_th * r_th + x_th * x_th;
let i1_re = 0.3_f64;
let i1_im = -0.05_f64;
let v1_re = v_th_re - (r_th * i1_re - x_th * i1_im);
let v1_im = v_th_im - (r_th * i1_im + x_th * i1_re);
let v1_mag = (v1_re * v1_re + v1_im * v1_im).sqrt();
let v1_ang_deg = v1_im.atan2(v1_re).to_degrees();
let p1_mw = (v1_re * i1_re + v1_im * i1_im) * 100.0;
let q1_mvar = (v1_im * i1_re - v1_re * i1_im) * 100.0;
let i2_re = 0.5_f64;
let i2_im = -0.1_f64;
let v2_re = v_th_re - (r_th * i2_re - x_th * i2_im);
let v2_im = v_th_im - (r_th * i2_im + x_th * i2_re);
let v2_mag = (v2_re * v2_re + v2_im * v2_im).sqrt();
let v2_ang_deg = v2_im.atan2(v2_re).to_degrees();
let p2_mw = (v2_re * i2_re + v2_im * i2_im) * 100.0;
let q2_mvar = (v2_im * i2_re - v2_re * i2_im) * 100.0;
let extractor = NetworkEquivalentExtractor::new(100.0, vec![110.0]);
let th = extractor
.estimate_from_measurements(
0,
(v1_mag, v1_ang_deg, p1_mw, q1_mvar),
(v2_mag, v2_ang_deg, p2_mw, q2_mvar),
110.0,
)
.expect("measurement estimation failed");
let (r_est, x_est) = th.z_th_pu;
let z_mag = z_mag2.sqrt();
let r_err = (r_est - r_th).abs() / z_mag;
let x_err = (x_est - x_th).abs() / z_mag;
assert!(
r_err < 0.05,
"R_th estimate error {r_err:.3} > 5%: got {r_est:.6}, expected {r_th}"
);
assert!(
x_err < 0.05,
"X_th estimate error {x_err:.3} > 5%: got {x_est:.6}, expected {x_th}"
);
}
#[test]
fn test_short_circuit_mva() {
let r = 0.01_f64;
let x = 0.1_f64;
let z2 = r * r + x * x;
let g = r / z2;
let b = -x / z2;
let sh = 1e-4;
let y_bus = vec![vec![(g + sh, b), (-g, -b)], vec![(-g, -b), (g + sh, b)]];
let base_mva = 100.0_f64;
let base_kv = 110.0_f64;
let extractor = NetworkEquivalentExtractor::new(base_mva, vec![base_kv, base_kv]);
let th = extractor
.thevenin_at_bus(0, &y_bus, 1.0, 0.0)
.expect("thevenin failed");
let z_mag = (th.z_th_pu.0 * th.z_th_pu.0 + th.z_th_pu.1 * th.z_th_pu.1).sqrt();
let expected_sc_mva = th.v_th_pu * th.v_th_pu / z_mag * base_mva;
let error_pct = (th.short_circuit_mva - expected_sc_mva).abs() / expected_sc_mva * 100.0;
assert!(
error_pct < 1.0,
"S_sc={:.2} MVA, expected={:.2} MVA, error={:.2}%",
th.short_circuit_mva,
expected_sc_mva,
error_pct
);
}
#[test]
fn thevenin_reactance_is_positive_for_inductive_network() {
let r = 0.001_f64;
let x = 0.1_f64;
let z2 = r * r + x * x;
let g = r / z2;
let b = -x / z2;
let sh = 1e-4;
let y_bus = vec![vec![(g + sh, b), (-g, -b)], vec![(-g, -b), (g + sh, b)]];
let extractor = NetworkEquivalentExtractor::new(100.0, vec![110.0, 110.0]);
let th = extractor
.thevenin_at_bus(0, &y_bus, 1.0, 0.0)
.expect("thevenin_at_bus should succeed for well-formed Y-bus");
let (_r_th, x_th) = th.z_th_pu;
assert!(
x_th > 0.0,
"Thévenin reactance must be positive for inductive network, got {x_th:.6}"
);
assert!(
th.z_th_pu.0.is_finite() && th.z_th_pu.1.is_finite(),
"Thévenin impedance components must be finite"
);
}
#[test]
fn thevenin_at_multiple_buses_different_values() {
let r = 0.02_f64;
let x = 0.2_f64;
let z2 = r * r + x * x;
let g = r / z2;
let b = -x / z2;
let sh = 1e-4;
let y_bus = vec![
vec![(2.0 * g + sh, 2.0 * b), (-g, -b), (-g, -b)],
vec![(-g, -b), (2.0 * g + sh, 2.0 * b), (-g, -b)],
vec![(-g, -b), (-g, -b), (2.0 * g + sh, 2.0 * b)],
];
let extractor = NetworkEquivalentExtractor::new(100.0, vec![110.0, 110.0, 110.0]);
let th0 = extractor
.thevenin_at_bus(0, &y_bus, 1.0, 0.0)
.expect("thevenin at bus 0 should succeed");
let th1 = extractor
.thevenin_at_bus(1, &y_bus, 0.99, -2.0)
.expect("thevenin at bus 1 should succeed");
let (_, x0) = th0.z_th_pu;
let (_, x1) = th1.z_th_pu;
assert!(x0 > 0.0, "X_th at bus 0 must be positive, got {x0:.6}");
assert!(x1 > 0.0, "X_th at bus 1 must be positive, got {x1:.6}");
assert_eq!(th0.bus, 0);
assert_eq!(th1.bus, 1);
assert!(
(th0.v_th_pu - 1.0).abs() < 1e-10,
"v_th_pu at bus 0 should equal 1.0"
);
assert!(
(th1.v_th_pu - 0.99).abs() < 1e-10,
"v_th_pu at bus 1 should equal 0.99"
);
}
#[test]
fn thevenin_short_circuit_ka_consistent_with_mva() {
let r = 0.01_f64;
let x = 0.1_f64;
let z2 = r * r + x * x;
let g = r / z2;
let b = -x / z2;
let sh = 1e-4;
let y_bus = vec![vec![(g + sh, b), (-g, -b)], vec![(-g, -b), (g + sh, b)]];
let base_kv = 110.0_f64;
let extractor = NetworkEquivalentExtractor::new(100.0, vec![base_kv, base_kv]);
let th = extractor
.thevenin_at_bus(0, &y_bus, 1.0, 0.0)
.expect("thevenin_at_bus should succeed");
let expected_ka = th.short_circuit_mva / (3.0_f64.sqrt() * base_kv);
assert!(
(th.short_circuit_ka - expected_ka).abs() < 1e-6,
"short_circuit_ka={:.6} kA does not match S_sc / (√3 × V_base) = {:.6} kA",
th.short_circuit_ka,
expected_ka
);
}
#[test]
fn thevenin_out_of_range_bus_returns_error() {
let r = 0.01_f64;
let x = 0.1_f64;
let z2 = r * r + x * x;
let g = r / z2;
let b = -x / z2;
let sh = 1e-4;
let y_bus = vec![vec![(g + sh, b), (-g, -b)], vec![(-g, -b), (g + sh, b)]];
let extractor = NetworkEquivalentExtractor::new(100.0, vec![110.0, 110.0]);
let result = extractor.thevenin_at_bus(5, &y_bus, 1.0, 0.0);
assert!(
matches!(result, Err(EquivalentError::BusOutOfRange(5))),
"expected BusOutOfRange(5), got {:?}",
result
);
}
#[test]
fn thevenin_impedance_components_are_finite() {
let r = 0.01_f64;
let x = 0.1_f64;
let z2 = r * r + x * x;
let g = r / z2;
let b = -x / z2;
let sh = 1e-4;
let y_bus = vec![vec![(g + sh, b), (-g, -b)], vec![(-g, -b), (g + sh, b)]];
let extractor = NetworkEquivalentExtractor::new(100.0, vec![110.0, 110.0]);
for bus in [0_usize, 1_usize] {
let th = extractor
.thevenin_at_bus(bus, &y_bus, 1.0, 0.0)
.expect("thevenin_at_bus must succeed for well-formed 2-bus Y-bus");
let (r_th, x_th) = th.z_th_pu;
assert!(
r_th.is_finite(),
"bus {bus}: R_th must be finite, got {r_th}"
);
assert!(
x_th.is_finite(),
"bus {bus}: X_th must be finite, got {x_th}"
);
}
}
#[test]
fn thevenin_r_component_is_nonnegative() {
let cases: &[(f64, f64)] = &[(0.001, 0.1), (0.05, 0.1), (0.1, 0.1)];
for &(r, x) in cases {
let z2 = r * r + x * x;
let g = r / z2;
let b = -x / z2;
let sh = 1e-4;
let y_bus = vec![vec![(g + sh, b), (-g, -b)], vec![(-g, -b), (g + sh, b)]];
let extractor = NetworkEquivalentExtractor::new(100.0, vec![110.0, 110.0]);
let th = extractor
.thevenin_at_bus(0, &y_bus, 1.0, 0.0)
.expect("thevenin_at_bus must succeed");
let (r_th, _) = th.z_th_pu;
assert!(
r_th >= 0.0,
"R_th must be >= 0 for passive network (r={r}, x={x}), got {r_th:.8}"
);
}
}
#[test]
fn thevenin_physical_impedance_scales_with_base_kv() {
let r = 0.01_f64;
let x = 0.1_f64;
let z2 = r * r + x * x;
let g = r / z2;
let b = -x / z2;
let sh = 1e-4;
let y_bus = vec![vec![(g + sh, b), (-g, -b)], vec![(-g, -b), (g + sh, b)]];
let extractor_110 = NetworkEquivalentExtractor::new(100.0, vec![110.0, 110.0]);
let extractor_220 = NetworkEquivalentExtractor::new(100.0, vec![220.0, 220.0]);
let th_110 = extractor_110
.thevenin_at_bus(0, &y_bus, 1.0, 0.0)
.expect("thevenin at 110 kV must succeed");
let th_220 = extractor_220
.thevenin_at_bus(0, &y_bus, 1.0, 0.0)
.expect("thevenin at 220 kV must succeed");
let (r_pu_110, x_pu_110) = th_110.z_th_pu;
let (r_pu_220, x_pu_220) = th_220.z_th_pu;
assert!(
(r_pu_110 - r_pu_220).abs() < 1e-10,
"R_th_pu must be identical regardless of base_kv: got {r_pu_110:.8} vs {r_pu_220:.8}"
);
assert!(
(x_pu_110 - x_pu_220).abs() < 1e-10,
"X_th_pu must be identical regardless of base_kv: got {x_pu_110:.8} vs {x_pu_220:.8}"
);
let (r_ohm_110, _) = th_110.z_th_ohm;
let (r_ohm_220, _) = th_220.z_th_ohm;
if r_ohm_110.abs() > 1e-12 {
let ratio = r_ohm_220 / r_ohm_110;
assert!(
(ratio - 4.0).abs() < 1e-6,
"z_th_ohm must scale as (kV)²: 220kV/110kV ratio = {ratio:.6}, expected 4.0"
);
}
}
#[test]
fn norton_conductance_is_nonnegative() {
let r = 0.01_f64;
let x = 0.1_f64;
let z2 = r * r + x * x;
let g = r / z2;
let b = -x / z2;
let sh = 1e-4;
let y_bus = vec![vec![(g + sh, b), (-g, -b)], vec![(-g, -b), (g + sh, b)]];
let extractor = NetworkEquivalentExtractor::new(100.0, vec![110.0, 110.0]);
let th = extractor
.thevenin_at_bus(0, &y_bus, 1.0, 0.0)
.expect("thevenin_at_bus must succeed");
let norton = extractor.norton_at_bus(&th);
let (g_n, _) = norton.y_n_siemens;
assert!(
g_n >= 0.0,
"Norton conductance G_N must be >= 0 for passive network, got {g_n:.8}"
);
}
#[test]
fn thevenin_with_zero_base_kv_returns_error() {
let r = 0.01_f64;
let x = 0.1_f64;
let z2 = r * r + x * x;
let g = r / z2;
let b = -x / z2;
let sh = 1e-4;
let y_bus = vec![vec![(g + sh, b), (-g, -b)], vec![(-g, -b), (g + sh, b)]];
let extractor = NetworkEquivalentExtractor::new(100.0, vec![0.0, 110.0]);
let result = extractor.thevenin_at_bus(0, &y_bus, 1.0, 0.0);
assert!(
matches!(result, Err(EquivalentError::ZeroBaseKv(0))),
"expected ZeroBaseKv(0), got {:?}",
result
);
}
#[test]
fn estimate_from_measurements_insufficient_data_returns_error() {
let op = (1.0_f64, 0.0_f64, 100.0_f64, 20.0_f64);
let extractor = NetworkEquivalentExtractor::new(100.0, vec![110.0]);
let result = extractor.estimate_from_measurements(0, op, op, 110.0);
assert!(
matches!(result, Err(EquivalentError::InsufficientMeasurements)),
"expected InsufficientMeasurements for identical operating points, got {:?}",
result
);
}
}