use num_complex::Complex;
use crate::network::branch_calc_param;
use crate::types::{Bus, Line, Transformer};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Terminal {
From,
To,
}
#[derive(Clone, Copy, Debug)]
pub struct BranchParams {
pub from: usize,
pub to: usize,
pub y: [Complex<f64>; 4],
}
impl BranchParams {
pub(crate) fn seen_from(&self, terminal: Terminal) -> (Complex<f64>, Complex<f64>) {
match terminal {
Terminal::From => (self.y[0], self.y[1]),
Terminal::To => (self.y[3], self.y[2]),
}
}
pub(crate) fn buses(&self, terminal: Terminal) -> (usize, usize) {
match terminal {
Terminal::From => (self.from, self.to),
Terminal::To => (self.to, self.from),
}
}
}
pub fn line_params(line: &Line) -> BranchParams {
let zero = Complex::new(0.0, 0.0);
if line.from == line.to {
return BranchParams {
from: line.from,
to: line.to,
y: [Complex::new(line.g_shunt, line.b_shunt), zero, zero, zero],
};
}
let y_series = Complex::new(1.0, 0.0) / Complex::new(line.r, line.x);
let y_shunt_half = Complex::new(line.g_shunt / 2.0, line.b_shunt / 2.0);
let y_diag = y_series + y_shunt_half;
BranchParams {
from: line.from,
to: line.to,
y: [y_diag, -y_series, -y_series, y_diag],
}
}
pub fn transformer_params(t: &Transformer) -> BranchParams {
BranchParams {
from: t.from,
to: t.to,
y: branch_calc_param(t.y_series, t.y_shunt, t.tap, t.from_status, t.to_status),
}
}
pub fn branch_params(lines: &[Line], transformers: &[Transformer]) -> Vec<BranchParams> {
lines
.iter()
.map(line_params)
.chain(transformers.iter().map(transformer_params))
.collect()
}
pub fn bus_voltages(buses: &[Bus]) -> Vec<Complex<f64>> {
buses
.iter()
.map(|b| Complex::from_polar(b.voltage_mag, b.voltage_ang))
.collect()
}
pub fn terminal_flow(branch: &BranchParams, terminal: Terminal, v: &[Complex<f64>]) -> (f64, f64) {
let (near, far) = branch.buses(terminal);
let (y_self, y_mut) = branch.seen_from(terminal);
let (v_near, th_near) = (v[near].norm(), v[near].arg());
let (v_far, th_far) = (v[far].norm(), v[far].arg());
let (s, c) = (th_near - th_far).sin_cos();
let p = v_near * v_near * y_self.re + v_near * v_far * (y_mut.re * c + y_mut.im * s);
let q = -v_near * v_near * y_self.im + v_near * v_far * (y_mut.re * s - y_mut.im * c);
(p, q)
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct FlowDerivs {
pub dp_dtheta_near: f64,
pub dp_dtheta_far: f64,
pub dp_dv_near: f64,
pub dp_dv_far: f64,
pub dq_dtheta_near: f64,
pub dq_dtheta_far: f64,
pub dq_dv_near: f64,
pub dq_dv_far: f64,
}
pub fn terminal_flow_derivs(
branch: &BranchParams,
terminal: Terminal,
v: &[Complex<f64>],
) -> FlowDerivs {
let (near, far) = branch.buses(terminal);
let (y_self, y_mut) = branch.seen_from(terminal);
let (v_near, th_near) = (v[near].norm(), v[near].arg());
let (v_far, th_far) = (v[far].norm(), v[far].arg());
let (s, c) = (th_near - th_far).sin_cos();
let cos_term = y_mut.re * c + y_mut.im * s;
let sin_term = y_mut.re * s - y_mut.im * c;
FlowDerivs {
dp_dtheta_near: -v_near * v_far * sin_term,
dp_dtheta_far: v_near * v_far * sin_term,
dp_dv_near: 2.0 * v_near * y_self.re + v_far * cos_term,
dp_dv_far: v_near * cos_term,
dq_dtheta_near: v_near * v_far * cos_term,
dq_dtheta_far: -v_near * v_far * cos_term,
dq_dv_near: -2.0 * v_near * y_self.im + v_far * sin_term,
dq_dv_far: v_near * sin_term,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn test_line() -> Line {
Line {
from: 0,
to: 1,
r: 0.05,
x: 0.2,
b_shunt: 0.04,
g_shunt: 0.01,
}
}
fn test_voltages() -> Vec<Complex<f64>> {
vec![
Complex::from_polar(1.03, 0.07),
Complex::from_polar(0.97, -0.05),
]
}
#[test]
fn terminal_flows_sum_to_bus_injection() {
let line = test_line();
let p = line_params(&line);
let v = test_voltages();
let (p_from, q_from) = terminal_flow(&p, Terminal::From, &v);
let (p_to, q_to) = terminal_flow(&p, Terminal::To, &v);
let i0 = p.y[0] * v[0] + p.y[1] * v[1];
let s0 = v[0] * i0.conj();
let i1 = p.y[2] * v[0] + p.y[3] * v[1];
let s1 = v[1] * i1.conj();
assert!((p_from - s0.re).abs() < 1e-12, "p_from vs injection");
assert!((q_from - s0.im).abs() < 1e-12, "q_from vs injection");
assert!((p_to - s1.re).abs() < 1e-12, "p_to vs injection");
assert!((q_to - s1.im).abs() < 1e-12, "q_to vs injection");
}
#[test]
fn lossless_branch_conserves_active_power() {
let line = Line {
from: 0,
to: 1,
r: 0.0,
x: 0.2,
b_shunt: 0.0,
g_shunt: 0.0,
};
let p = line_params(&line);
let v = test_voltages();
let (p_from, _) = terminal_flow(&p, Terminal::From, &v);
let (p_to, _) = terminal_flow(&p, Terminal::To, &v);
assert!((p_from + p_to).abs() < 1e-12, "p_from={p_from} p_to={p_to}");
}
#[test]
fn derivs_match_finite_differences() {
let branch = line_params(&test_line());
let base = test_voltages();
for terminal in [Terminal::From, Terminal::To] {
let d = terminal_flow_derivs(&branch, terminal, &base);
let (near, far) = branch.buses(terminal);
let h = 1e-6;
let bump = |bus: usize, d_mag: f64, d_ang: f64| {
let mut v = base.clone();
v[bus] = Complex::from_polar(base[bus].norm() + d_mag, base[bus].arg() + d_ang);
terminal_flow(&branch, terminal, &v)
};
let central = |bus: usize, mag: bool| {
let (pp, qp) = if mag {
bump(bus, h, 0.0)
} else {
bump(bus, 0.0, h)
};
let (pm, qm) = if mag {
bump(bus, -h, 0.0)
} else {
bump(bus, 0.0, -h)
};
((pp - pm) / (2.0 * h), (qp - qm) / (2.0 * h))
};
let (dp_dth_near, dq_dth_near) = central(near, false);
let (dp_dth_far, dq_dth_far) = central(far, false);
let (dp_dv_near, dq_dv_near) = central(near, true);
let (dp_dv_far, dq_dv_far) = central(far, true);
let close = |a: f64, b: f64, what: &str| {
assert!(
(a - b).abs() < 1e-6,
"{terminal:?} {what}: analytic {a}, numeric {b}"
);
};
close(d.dp_dtheta_near, dp_dth_near, "dP/dtheta_near");
close(d.dp_dtheta_far, dp_dth_far, "dP/dtheta_far");
close(d.dp_dv_near, dp_dv_near, "dP/dv_near");
close(d.dp_dv_far, dp_dv_far, "dP/dv_far");
close(d.dq_dtheta_near, dq_dth_near, "dQ/dtheta_near");
close(d.dq_dtheta_far, dq_dth_far, "dQ/dtheta_far");
close(d.dq_dv_near, dq_dv_near, "dQ/dv_near");
close(d.dq_dv_far, dq_dv_far, "dQ/dv_far");
}
}
#[test]
fn self_loop_line_is_shunt_only() {
let line = Line {
from: 2,
to: 2,
r: 0.0,
x: 0.0,
b_shunt: 0.03,
g_shunt: 0.0,
};
let p = line_params(&line);
assert_eq!(p.y[1], Complex::new(0.0, 0.0));
assert_eq!(p.y[3], Complex::new(0.0, 0.0));
let v = vec![Complex::new(1.0, 0.0); 3];
let (p_from, q_from) = terminal_flow(&p, Terminal::From, &v);
assert!(p_from.abs() < 1e-12);
assert!((q_from + 0.03).abs() < 1e-12, "q_from={q_from}");
assert_eq!(terminal_flow(&p, Terminal::To, &v), (0.0, 0.0));
}
#[test]
fn transformer_tap_breaks_terminal_symmetry() {
let t = Transformer {
from: 0,
to: 1,
from_status: 1,
to_status: 1,
y_series: Complex::new(1.0, 0.0) / Complex::new(0.01, 0.1),
y_shunt: Complex::new(0.0, 0.0),
tap: Complex::new(1.05, 0.0),
};
let p = transformer_params(&t);
assert!(
(p.y[0] - p.y[3]).norm() > 1e-9,
"tap should make yff != ytt"
);
let v = test_voltages();
let (p_from, _) = terminal_flow(&p, Terminal::From, &v);
let (p_to, _) = terminal_flow(&p, Terminal::To, &v);
assert!(p_from + p_to > 0.0, "losses should be positive");
}
}