use crate::branch_flow::{BranchParams, Terminal, branch_params, terminal_flow_derivs};
use crate::jacobian::JacobianPattern;
use crate::network::{YBusSparse, power_injections};
use crate::types::{Bus, BusType, Line, Transformer};
use gridoxide_sparse::sparse::RealFactorization;
use num_complex::Complex;
const ZERO: Complex<f64> = Complex::new(0.0, 0.0);
const NOT_UNKNOWN: usize = usize::MAX;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Variable {
ActiveInjection(usize),
ReactiveInjection(usize),
TransformerRatio(usize),
PhaseShift(usize),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum TapKind {
Ratio,
Phase,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Function {
BranchActivePower { branch: usize, terminal: Terminal },
BranchReactivePower { branch: usize, terminal: Terminal },
VoltageMagnitude(usize),
VoltageAngle(usize),
}
#[derive(Clone, Debug)]
pub struct StateResponse {
pub d_theta: Vec<f64>,
pub d_vmag: Vec<f64>,
}
#[derive(Clone, Debug)]
pub struct FunctionRow {
pub d_active: Vec<f64>,
pub d_reactive: Vec<f64>,
pub d_ratio: Vec<f64>,
pub d_phase: Vec<f64>,
}
pub struct AcSensitivity {
n_buses: usize,
branches: Vec<BranchParams>,
taps: Vec<Option<Complex<f64>>>,
v: Vec<Complex<f64>>,
theta_pos: Vec<usize>,
vmag_pos: Vec<usize>,
n_unknowns: usize,
factorization: RealFactorization,
}
impl AcSensitivity {
pub fn new(
buses: &[Bus],
ybus: &YBusSparse,
lines: &[Line],
transformers: &[Transformer],
) -> Option<Self> {
let n = buses.len();
let branches = branch_params(lines, transformers);
let taps: Vec<Option<Complex<f64>>> = std::iter::repeat_n(None, lines.len())
.chain(transformers.iter().map(|t| Some(t.tap)))
.collect();
let mut theta_pos = vec![NOT_UNKNOWN; n];
let mut vmag_pos = vec![NOT_UNKNOWN; n];
let mut next = 0;
for b in buses {
if b.bus_type != BusType::Slack {
theta_pos[b.idx] = next;
next += 1;
}
}
for b in buses {
if b.bus_type == BusType::PQ {
vmag_pos[b.idx] = next;
next += 1;
}
}
let n_unknowns = next;
let (p_calc, q_calc) = power_injections(buses, ybus);
let pattern = JacobianPattern::analyze(buses, ybus);
let mut values = Vec::with_capacity(pattern.len());
pattern.fill(buses, &p_calc, &q_calc, &mut values);
let factorization = RealFactorization::new(n_unknowns, &pattern.to_triplets(&values))?;
Some(Self {
n_buses: n,
branches,
taps,
v: crate::branch_flow::bus_voltages(buses),
theta_pos,
vmag_pos,
n_unknowns,
factorization,
})
}
pub fn n_buses(&self) -> usize {
self.n_buses
}
pub fn n_branches(&self) -> usize {
self.branches.len()
}
fn variable_row(&self, variable: Variable) -> Option<usize> {
let row = match variable {
Variable::ActiveInjection(bus) => *self.theta_pos.get(bus)?,
Variable::ReactiveInjection(bus) => *self.vmag_pos.get(bus)?,
_ => return None,
};
(row != NOT_UNKNOWN).then_some(row)
}
fn admittance_derivative(&self, branch: usize, kind: TapKind) -> Option<[Complex<f64>; 4]> {
let tap = (*self.taps.get(branch)?)?;
let y = &self.branches[branch].y;
Some(match kind {
TapKind::Ratio => {
let k = tap.norm();
[-2.0 * y[0] / k, -y[1] / k, -y[2] / k, ZERO]
}
TapKind::Phase => {
let j = Complex::new(0.0, 1.0);
[ZERO, j * y[1], -j * y[2], ZERO]
}
})
}
fn tap_injection_derivative(
&self,
branch: usize,
kind: TapKind,
) -> Option<(Complex<f64>, Complex<f64>)> {
let dy = self.admittance_derivative(branch, kind)?;
let (f, t) = (self.branches[branch].from, self.branches[branch].to);
let (vf, vt) = (self.v[f], self.v[t]);
Some((
vf * (dy[0] * vf + dy[1] * vt).conj(),
vt * (dy[2] * vf + dy[3] * vt).conj(),
))
}
fn tap_residual_derivative(&self, branch: usize, kind: TapKind) -> Option<Vec<f64>> {
let (ds_from, ds_to) = self.tap_injection_derivative(branch, kind)?;
let (f, t) = (self.branches[branch].from, self.branches[branch].to);
let mut dg = vec![0.0; self.n_unknowns];
let mut place = |pos: usize, value: f64| {
if pos != NOT_UNKNOWN {
dg[pos] += value;
}
};
place(self.theta_pos[f], ds_from.re);
place(self.vmag_pos[f], ds_from.im);
place(self.theta_pos[t], ds_to.re);
place(self.vmag_pos[t], ds_to.im);
Some(dg)
}
fn as_tap(variable: Variable) -> Option<(usize, TapKind)> {
match variable {
Variable::TransformerRatio(b) => Some((b, TapKind::Ratio)),
Variable::PhaseShift(b) => Some((b, TapKind::Phase)),
_ => None,
}
}
pub fn state_response(&self, variable: Variable) -> Option<StateResponse> {
let zero_response = || StateResponse {
d_theta: vec![0.0; self.n_buses],
d_vmag: vec![0.0; self.n_buses],
};
if let Some((branch, kind)) = Self::as_tap(variable) {
if branch >= self.branches.len() {
return None;
}
let Some(dg) = self.tap_residual_derivative(branch, kind) else {
return Some(zero_response());
};
let mut dx = self.factorization.solve(&dg)?;
for value in &mut dx {
*value = -*value;
}
return Some(self.scatter(&dx));
}
let bus = match variable {
Variable::ActiveInjection(b) | Variable::ReactiveInjection(b) => b,
_ => unreachable!("tap variables returned above"),
};
if bus >= self.n_buses {
return None;
}
let Some(row) = self.variable_row(variable) else {
return Some(zero_response());
};
let mut rhs = vec![0.0; self.n_unknowns];
rhs[row] = 1.0;
let dx = self.factorization.solve(&rhs)?;
Some(self.scatter(&dx))
}
fn scatter(&self, dx: &[f64]) -> StateResponse {
let mut d_theta = vec![0.0; self.n_buses];
let mut d_vmag = vec![0.0; self.n_buses];
for i in 0..self.n_buses {
if self.theta_pos[i] != NOT_UNKNOWN {
d_theta[i] = dx[self.theta_pos[i]];
}
if self.vmag_pos[i] != NOT_UNKNOWN {
d_vmag[i] = dx[self.vmag_pos[i]];
}
}
StateResponse { d_theta, d_vmag }
}
pub fn branch_response(
&self,
variable: Variable,
terminal: Terminal,
) -> Option<Vec<(f64, f64)>> {
let state = self.state_response(variable)?;
let direct = Self::as_tap(variable).and_then(|(branch, kind)| {
Some((branch, self.tap_flow_derivative(branch, kind, terminal)?))
});
Some(
self.branches
.iter()
.enumerate()
.map(|(i, branch)| {
let d = terminal_flow_derivs(branch, terminal, &self.v);
let (near, far) = branch.buses(terminal);
let mut dp = d.dp_dtheta_near * state.d_theta[near]
+ d.dp_dtheta_far * state.d_theta[far]
+ d.dp_dv_near * state.d_vmag[near]
+ d.dp_dv_far * state.d_vmag[far];
let mut dq = d.dq_dtheta_near * state.d_theta[near]
+ d.dq_dtheta_far * state.d_theta[far]
+ d.dq_dv_near * state.d_vmag[near]
+ d.dq_dv_far * state.d_vmag[far];
if let Some((tapped, s)) = direct {
if tapped == i {
dp += s.re;
dq += s.im;
}
}
(dp, dq)
})
.collect(),
)
}
fn tap_flow_derivative(
&self,
branch: usize,
kind: TapKind,
terminal: Terminal,
) -> Option<Complex<f64>> {
let (ds_from, ds_to) = self.tap_injection_derivative(branch, kind)?;
Some(match terminal {
Terminal::From => ds_from,
Terminal::To => ds_to,
})
}
fn function_gradient(&self, function: Function) -> Option<Vec<f64>> {
let mut grad = vec![0.0; self.n_unknowns];
let mut place = |pos: usize, value: f64| {
if pos != NOT_UNKNOWN {
grad[pos] += value;
}
};
match function {
Function::BranchActivePower { branch, terminal }
| Function::BranchReactivePower { branch, terminal } => {
let params = self.branches.get(branch)?;
let d = terminal_flow_derivs(params, terminal, &self.v);
let (near, far) = params.buses(terminal);
let active = matches!(function, Function::BranchActivePower { .. });
let (dth_near, dth_far, dv_near, dv_far) = if active {
(d.dp_dtheta_near, d.dp_dtheta_far, d.dp_dv_near, d.dp_dv_far)
} else {
(d.dq_dtheta_near, d.dq_dtheta_far, d.dq_dv_near, d.dq_dv_far)
};
place(self.theta_pos[near], dth_near);
place(self.theta_pos[far], dth_far);
place(self.vmag_pos[near], dv_near);
place(self.vmag_pos[far], dv_far);
}
Function::VoltageMagnitude(bus) => {
place(*self.vmag_pos.get(bus)?, 1.0);
}
Function::VoltageAngle(bus) => {
place(*self.theta_pos.get(bus)?, 1.0);
}
}
Some(grad)
}
pub fn function_row(&self, function: Function) -> Option<FunctionRow> {
let grad = self.function_gradient(function)?;
let w = self.factorization.solve_transpose(&grad)?;
let mut d_active = vec![0.0; self.n_buses];
let mut d_reactive = vec![0.0; self.n_buses];
for i in 0..self.n_buses {
if self.theta_pos[i] != NOT_UNKNOWN {
d_active[i] = w[self.theta_pos[i]];
}
if self.vmag_pos[i] != NOT_UNKNOWN {
d_reactive[i] = w[self.vmag_pos[i]];
}
}
let mut d_ratio = vec![0.0; self.branches.len()];
let mut d_phase = vec![0.0; self.branches.len()];
for branch in 0..self.branches.len() {
for (kind, out) in [
(TapKind::Ratio, &mut d_ratio),
(TapKind::Phase, &mut d_phase),
] {
let Some(dg) = self.tap_residual_derivative(branch, kind) else {
continue;
};
let mut value: f64 = -dg.iter().zip(&w).map(|(g, wi)| g * wi).sum::<f64>();
if let Function::BranchActivePower {
branch: b,
terminal,
}
| Function::BranchReactivePower {
branch: b,
terminal,
} = function
{
if b == branch {
let s = self
.tap_flow_derivative(branch, kind, terminal)
.expect("the tap exists, it was read just above");
value += if matches!(function, Function::BranchActivePower { .. }) {
s.re
} else {
s.im
};
}
}
out[branch] = value;
}
}
Some(FunctionRow {
d_active,
d_reactive,
d_ratio,
d_phase,
})
}
}