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());
}
}