use num_complex::Complex;
use crate::branch_flow::Terminal;
use crate::measurement::Target;
use super::SeNetwork;
const J: Complex<f64> = Complex::new(0.0, 1.0);
#[derive(Clone, Debug, PartialEq)]
pub struct CurrentFunctional {
pub at: usize,
pub coefficients: Vec<(usize, Complex<f64>)>,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Partial {
pub bus: usize,
pub d_dtheta: Complex<f64>,
pub d_dvmag: Complex<f64>,
}
impl CurrentFunctional {
pub fn current(&self, v: &[Complex<f64>]) -> Complex<f64> {
self.coefficients.iter().map(|&(k, c)| c * v[k]).sum()
}
pub fn power(&self, v: &[Complex<f64>]) -> Complex<f64> {
v[self.at] * self.current(v).conj()
}
pub fn power_partials(&self, v: &[Complex<f64>], out: &mut Vec<Partial>) {
power_partials_of(self.at, &self.coefficients, v, out)
}
pub fn local_current(&self, v: &[Complex<f64>]) -> Complex<f64> {
local_current_of(self.at, &self.coefficients, v)
}
pub fn current_partials(&self, v: &[Complex<f64>], out: &mut Vec<Partial>) {
current_partials_of(&self.coefficients, v, out)
}
pub fn local_current_partials(&self, v: &[Complex<f64>], out: &mut Vec<Partial>) {
local_current_partials_of(self.at, &self.coefficients, v, out)
}
}
pub fn power_partials_of(
at: usize,
coefficients: &[(usize, Complex<f64>)],
v: &[Complex<f64>],
out: &mut Vec<Partial>,
) {
{
if coefficients.is_empty() {
return;
}
let v_at = v[at];
let s = v_at * coefficients.iter().map(|&(k, c)| c * v[k]).sum::<Complex<f64>>().conj();
let inv_mag = if v_at.norm() > 0.0 { v_at.norm().recip() } else { 0.0 };
let mut saw_at = false;
for &(k, c) in coefficients {
let unit_k = unit(v[k]);
let mut d_dtheta = -J * v_at * (c * v[k]).conj();
let mut d_dvmag = v_at * (c * unit_k).conj();
if k == at {
saw_at = true;
d_dtheta += J * s;
d_dvmag += s * inv_mag;
}
out.push(Partial { bus: k, d_dtheta, d_dvmag });
}
if !saw_at {
out.push(Partial { bus: at, d_dtheta: J * s, d_dvmag: s * inv_mag });
}
}
}
pub fn current_partials_of(
coefficients: &[(usize, Complex<f64>)],
v: &[Complex<f64>],
out: &mut Vec<Partial>,
) {
for &(k, c) in coefficients {
let unit_k = unit(v[k]);
out.push(Partial { bus: k, d_dtheta: J * c * v[k], d_dvmag: c * unit_k });
}
}
pub fn local_current_partials_of(
at: usize,
coefficients: &[(usize, Complex<f64>)],
v: &[Complex<f64>],
out: &mut Vec<Partial>,
) {
let start = out.len();
power_partials_of(at, coefficients, v, out);
if out.len() == start {
return;
}
let mag = v[at].norm();
if mag <= 0.0 {
return;
}
let s = local_current_of(at, coefficients, v) * mag;
for p in out[start..].iter_mut() {
p.d_dtheta /= mag;
p.d_dvmag /= mag;
if p.bus == at {
p.d_dvmag -= s / (mag * mag);
}
}
}
pub fn local_current_of(
at: usize,
coefficients: &[(usize, Complex<f64>)],
v: &[Complex<f64>],
) -> Complex<f64> {
let mag = v[at].norm();
if mag <= 0.0 {
return Complex::new(0.0, 0.0);
}
let i: Complex<f64> = coefficients.iter().map(|&(k, c)| c * v[k]).sum();
v[at] * i.conj() / mag
}
fn unit(z: Complex<f64>) -> Complex<f64> {
if z.norm() > 0.0 { z / z.norm() } else { Complex::new(1.0, 0.0) }
}
impl SeNetwork {
pub fn functional(&self, target: Target) -> Option<CurrentFunctional> {
Some(match target {
Target::Bus(bus) => {
CurrentFunctional { at: bus, coefficients: self.ybus.row(bus).to_vec() }
}
Target::BranchTerminal { branch, terminal }
| Target::BranchTerminalCurrent { branch, terminal, .. } => {
self.terminals.get(branch)?[terminal as usize].clone()
}
Target::SourceInjection { branch } => {
let f = &self.terminals.get(branch)?[Terminal::To as usize];
CurrentFunctional {
at: f.at,
coefficients: f.coefficients.iter().map(|&(k, c)| (k, -c)).collect(),
}
}
Target::ShuntInjection { bus } => CurrentFunctional {
at: bus,
coefficients: vec![(bus, -self.shunt_y[bus])],
},
Target::NodeInjection(bus) => {
let mut coefficients = self.ybus.row(bus).to_vec();
for &branch in &self.source_branches[bus] {
let f = &self.terminals[branch][Terminal::To as usize];
debug_assert_eq!(f.at, bus, "a source branch must feed the bus it is listed on");
coefficients.extend(f.coefficients.iter().map(|&(k, c)| (k, -c)));
}
coefficients.push((bus, -self.shunt_y[bus]));
CurrentFunctional { at: bus, coefficients }
}
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::branch_flow::{terminal_flow, terminal_flow_derivs};
fn state(buses: &[crate::types::Bus]) -> Vec<Complex<f64>> {
buses.iter().map(|b| Complex::from_polar(b.voltage_mag, b.voltage_ang)).collect()
}
#[test]
fn a_branch_terminal_functional_matches_terminal_flow() {
let (net, mut buses) = super::super::tests::two_bus_net();
buses[0].voltage_mag = 1.04;
buses[0].voltage_ang = 0.11;
buses[1].voltage_mag = 0.97;
buses[1].voltage_ang = -0.07;
let v = state(&buses);
for terminal in [Terminal::From, Terminal::To] {
let f = net
.functional(Target::BranchTerminal { branch: 0, terminal })
.expect("branch 0 exists");
let s = f.power(&v);
let (p, q) = terminal_flow(&super::super::tests::two_bus_params(), terminal, &v);
assert!((s.re - p).abs() < 1e-12, "{terminal:?}: P {} vs {p}", s.re);
assert!((s.im - q).abs() < 1e-12, "{terminal:?}: Q {} vs {q}", s.im);
}
}
#[test]
fn branch_terminal_partials_match_the_closed_form() {
let (net, mut buses) = super::super::tests::two_bus_net();
buses[0].voltage_mag = 1.04;
buses[0].voltage_ang = 0.11;
buses[1].voltage_mag = 0.97;
buses[1].voltage_ang = -0.07;
let v = state(&buses);
for terminal in [Terminal::From, Terminal::To] {
let f = net
.functional(Target::BranchTerminal { branch: 0, terminal })
.expect("branch 0 exists");
let mut partials = Vec::new();
f.power_partials(&v, &mut partials);
let params = super::super::tests::two_bus_params();
let d = terminal_flow_derivs(¶ms, terminal, &v);
let (near, _far) = params.buses(terminal);
let want = |bus: usize| -> (f64, f64, f64, f64) {
if bus == near {
(d.dp_dtheta_near, d.dq_dtheta_near, d.dp_dv_near, d.dq_dv_near)
} else {
(d.dp_dtheta_far, d.dq_dtheta_far, d.dp_dv_far, d.dq_dv_far)
}
};
assert_eq!(partials.len(), 2, "a two-bus branch touches two buses");
for p in &partials {
let (dp_dth, dq_dth, dp_dv, dq_dv) = want(p.bus);
assert!((p.d_dtheta.re - dp_dth).abs() < 1e-10, "dP/dθ at {}", p.bus);
assert!((p.d_dtheta.im - dq_dth).abs() < 1e-10, "dQ/dθ at {}", p.bus);
assert!((p.d_dvmag.re - dp_dv).abs() < 1e-10, "dP/dV at {}", p.bus);
assert!((p.d_dvmag.im - dq_dv).abs() < 1e-10, "dQ/dV at {}", p.bus);
}
}
}
#[test]
fn partials_match_finite_differences_for_every_target() {
let (net, mut buses) = super::super::tests::two_bus_net();
buses[0].voltage_mag = 1.03;
buses[0].voltage_ang = 0.13;
buses[1].voltage_mag = 0.96;
buses[1].voltage_ang = -0.09;
let targets = [
Target::Bus(1),
Target::NodeInjection(1),
Target::ShuntInjection { bus: 1 },
Target::BranchTerminal { branch: 0, terminal: Terminal::From },
Target::BranchTerminal { branch: 0, terminal: Terminal::To },
Target::SourceInjection { branch: 0 },
];
const H: f64 = 1e-6;
for target in targets {
let f = net.functional(target).expect("target resolves");
let mut raw = Vec::new();
f.power_partials(&state(&buses), &mut raw);
let mut partials: Vec<Partial> = Vec::new();
for e in &raw {
match partials.iter_mut().find(|p| p.bus == e.bus) {
Some(p) => {
p.d_dtheta += e.d_dtheta;
p.d_dvmag += e.d_dvmag;
}
None => partials.push(*e),
}
}
if matches!(target, Target::NodeInjection(_)) {
assert!(raw.len() > partials.len(), "the duplicate-summing path should be exercised");
}
for p in &partials {
let mut shifted = buses.clone();
shifted[p.bus].voltage_ang += H;
let up = f.power(&state(&shifted));
shifted[p.bus].voltage_ang -= 2.0 * H;
let down = f.power(&state(&shifted));
let fd = (up - down) / (2.0 * H);
assert!(
(fd - p.d_dtheta).norm() < 1e-5,
"{target:?} d/dθ at bus {}: analytic {} vs finite difference {fd}",
p.bus,
p.d_dtheta
);
let mut shifted = buses.clone();
shifted[p.bus].voltage_mag += H;
let up = f.power(&state(&shifted));
shifted[p.bus].voltage_mag -= 2.0 * H;
let down = f.power(&state(&shifted));
let fd = (up - down) / (2.0 * H);
assert!(
(fd - p.d_dvmag).norm() < 1e-5,
"{target:?} d/d|V| at bus {}: analytic {} vs finite difference {fd}",
p.bus,
p.d_dvmag
);
}
}
}
#[test]
fn local_current_is_the_power_over_the_magnitude() {
let (net, mut buses) = super::super::tests::two_bus_net();
buses[0].voltage_mag = 1.041;
buses[0].voltage_ang = 0.137;
buses[1].voltage_mag = 0.958;
buses[1].voltage_ang = -0.061;
let v = state(&buses);
let f = net
.functional(Target::BranchTerminal { branch: 0, terminal: Terminal::From })
.expect("branch 0");
let local = f.local_current(&v);
let s = f.power(&v);
let i = f.current(&v);
let v_at = v[f.at];
assert!((local - s / v_at.norm()).norm() < 1e-12, "S/|V| identity");
assert!(
(local - i.conj() * Complex::from_polar(1.0, v_at.arg())).norm() < 1e-12,
"conj(I)·e^{{jθ}} identity"
);
assert!((local - i).norm() > 1e-3, "the two frames should differ here");
}
#[test]
fn current_partials_match_finite_differences() {
let (net, mut buses) = super::super::tests::two_bus_net();
buses[0].voltage_mag = 1.031;
buses[0].voltage_ang = 0.121;
buses[1].voltage_mag = 0.967;
buses[1].voltage_ang = -0.074;
let f = net
.functional(Target::BranchTerminal { branch: 0, terminal: Terminal::To })
.expect("branch 0");
const H: f64 = 1e-6;
for (name, value, partials) in [
(
"global",
Box::new(|v: &[Complex<f64>]| f.current(v)) as Box<dyn Fn(&[Complex<f64>]) -> Complex<f64>>,
{
let mut o = Vec::new();
f.current_partials(&state(&buses), &mut o);
o
},
),
(
"local",
Box::new(|v: &[Complex<f64>]| f.local_current(v)),
{
let mut o = Vec::new();
f.local_current_partials(&state(&buses), &mut o);
o
},
),
] {
let mut summed: Vec<Partial> = Vec::new();
for e in &partials {
match summed.iter_mut().find(|p| p.bus == e.bus) {
Some(p) => {
p.d_dtheta += e.d_dtheta;
p.d_dvmag += e.d_dvmag;
}
None => summed.push(*e),
}
}
for p in &summed {
let mut shifted = buses.clone();
shifted[p.bus].voltage_ang += H;
let up = value(&state(&shifted));
shifted[p.bus].voltage_ang -= 2.0 * H;
let fd = (up - value(&state(&shifted))) / (2.0 * H);
assert!((fd - p.d_dtheta).norm() < 1e-5, "{name} d/dθ at {}", p.bus);
let mut shifted = buses.clone();
shifted[p.bus].voltage_mag += H;
let up = value(&state(&shifted));
shifted[p.bus].voltage_mag -= 2.0 * H;
let fd = (up - value(&state(&shifted))) / (2.0 * H);
assert!((fd - p.d_dvmag).norm() < 1e-5, "{name} d/d|V| at {}", p.bus);
}
}
}
#[test]
fn an_empty_functional_produces_no_partials() {
let f = CurrentFunctional { at: 0, coefficients: Vec::new() };
let v = vec![Complex::new(1.0, 0.0)];
assert_eq!(f.power(&v), Complex::new(0.0, 0.0));
let mut partials = Vec::new();
f.power_partials(&v, &mut partials);
assert!(partials.is_empty());
}
}