use crate::network::{YBusSparse, effective_injection, power_injections};
use crate::types::{Bus, BusType};
use gridoxide_sparse::lu::{LuBackend, SparseLu};
use super::augmented::{AugmentedPattern, Layout, Parametrization, border_columns};
use super::direction::{BaseSpec, LoadingDirection};
#[derive(Clone, Debug)]
pub(crate) enum Constraint {
Natural { target: f64 },
Local { k: usize, target: f64 },
Arc {
tangent: Vec<f64>,
z0: Vec<f64>,
step: f64,
},
}
impl Constraint {
fn residual(&self, layout: &Layout, buses: &[Bus], lambda: f64) -> f64 {
match self {
Constraint::Natural { target } => lambda - target,
Constraint::Local { k, target } => layout.get(buses, lambda, *k) - target,
Constraint::Arc { tangent, z0, step } => {
let mut acc = -step;
for k in 0..=layout.n_unknowns {
acc += tangent[k] * (layout.get(buses, lambda, k) - z0[k]);
}
acc
}
}
}
fn border(&self, layout: &Layout) -> (Vec<usize>, Vec<f64>) {
let n = layout.n_unknowns;
match self {
Constraint::Natural { .. } => {
(border_columns(Parametrization::Natural, n, n), vec![1.0])
}
Constraint::Local { k, .. } => {
(border_columns(Parametrization::Local, *k, n), vec![1.0])
}
Constraint::Arc { tangent, .. } => (
border_columns(Parametrization::PseudoArcLength, 0, n),
tangent[..=n].to_vec(),
),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) enum CorrectorStatus {
Converged,
MaxIterations,
Singular,
}
pub(crate) struct CorrectorOutcome {
pub status: CorrectorStatus,
pub iterations: usize,
pub lambda: f64,
}
pub(crate) struct Segment {
pub layout: Layout,
pattern: AugmentedPattern,
ds: Vec<f64>,
solver: SparseLu,
values: Vec<f64>,
pub solves: usize,
pub reanalyses: usize,
}
impl Segment {
pub fn analyze(
buses: &[Bus],
ybus: &YBusSparse,
dir: &LoadingDirection,
border_cols: Vec<usize>,
backend: LuBackend,
) -> Self {
let layout = Layout::analyze(buses);
let pattern = AugmentedPattern::analyze(buses, ybus, border_cols);
let ds = layout.direction_rows(&dir.d_p, &dir.d_q);
let values = Vec::with_capacity(pattern.len());
Self {
layout,
pattern,
ds,
solver: SparseLu::new(backend),
values,
solves: 0,
reanalyses: 0,
}
}
fn ensure_border(&mut self, buses: &[Bus], ybus: &YBusSparse, cols: &[usize]) {
if self.pattern.border_cols() == cols {
return;
}
self.pattern = AugmentedPattern::analyze(buses, ybus, cols.to_vec());
self.solver.reset();
self.reanalyses += 1;
}
pub fn n(&self) -> usize {
self.layout.n_unknowns
}
fn mismatch(&self, buses: &[Bus], ybus: &YBusSparse) -> (Vec<f64>, Vec<f64>, Vec<f64>) {
let (p_calc, q_calc) = power_injections(buses, ybus);
let mut m = vec![0.0; self.layout.n_unknowns];
for (row, &i) in self.layout.non_slack_idx.iter().enumerate() {
let (p_eff, _) = effective_injection(&buses[i]);
m[row] = p_eff - p_calc[i];
}
for (row, &i) in self.layout.pq_idx.iter().enumerate() {
let (_, q_eff) = effective_injection(&buses[i]);
m[self.layout.n_angle + row] = q_eff - q_calc[i];
}
(m, p_calc, q_calc)
}
fn solve_bordered(
&mut self,
buses: &[Bus],
ybus: &YBusSparse,
p_calc: &[f64],
q_calc: &[f64],
border_cols: &[usize],
border: &[f64],
rhs: &[f64],
) -> Option<Vec<f64>> {
self.ensure_border(buses, ybus, border_cols);
let (pattern, ds, values) = (&self.pattern, &self.ds, &mut self.values);
pattern.fill(buses, p_calc, q_calc, ds, border, values);
if !self.solver.is_analyzed()
&& !self
.solver
.analyze(pattern.dim(), &pattern.to_triplets(values))
{
return None;
}
self.solves += 1;
self.solver.factor_and_solve_values(values, rhs)
}
pub fn tangent(
&mut self,
buses: &[Bus],
ybus: &YBusSparse,
border_cols: &[usize],
border: &[f64],
sign: f64,
) -> Option<Vec<f64>> {
let (p_calc, q_calc) = power_injections(buses, ybus);
let mut rhs = vec![0.0; self.layout.n_unknowns + 1];
rhs[self.layout.n_unknowns] = sign;
let mut t =
self.solve_bordered(buses, ybus, &p_calc, &q_calc, border_cols, border, &rhs)?;
let norm = t.iter().map(|v| v * v).sum::<f64>().sqrt();
if !(norm > 0.0) || !norm.is_finite() {
return None;
}
for v in &mut t {
*v /= norm;
}
Some(t)
}
#[allow(clippy::too_many_arguments)]
pub fn correct(
&mut self,
buses: &mut [Bus],
ybus: &YBusSparse,
base: &BaseSpec,
dir: &LoadingDirection,
constraint: &Constraint,
mut lambda: f64,
tol: f64,
max_iter: usize,
) -> CorrectorOutcome {
let n = self.layout.n_unknowns;
for it in 0..max_iter {
dir.apply(base, lambda, buses);
let (mismatch, p_calc, q_calc) = self.mismatch(buses, ybus);
let p_res = constraint.residual(&self.layout, buses, lambda);
let worst = mismatch
.iter()
.fold(0.0f64, |a, &b| a.max(b.abs()))
.max(p_res.abs());
if worst < tol {
return CorrectorOutcome {
status: CorrectorStatus::Converged,
iterations: it,
lambda,
};
}
let (border_cols, border) = constraint.border(&self.layout);
let mut rhs = Vec::with_capacity(n + 1);
rhs.extend_from_slice(&mismatch);
rhs.push(-p_res);
let Some(dz) =
self.solve_bordered(buses, ybus, &p_calc, &q_calc, &border_cols, &border, &rhs)
else {
return CorrectorOutcome {
status: CorrectorStatus::Singular,
iterations: it,
lambda,
};
};
for k in 0..n {
self.layout.add(buses, k, dz[k]);
}
lambda += dz[n];
}
dir.apply(base, lambda, buses);
CorrectorOutcome {
status: CorrectorStatus::MaxIterations,
iterations: max_iter,
lambda,
}
}
pub fn q_injections(&self, buses: &[Bus], ybus: &YBusSparse) -> Vec<f64> {
power_injections(buses, ybus).1
}
}
pub(crate) fn zip_buses(buses: &[Bus]) -> Vec<usize> {
buses
.iter()
.filter(|b| b.bus_type != BusType::Slack && !b.zip_terms.is_empty())
.map(|b| b.idx)
.collect()
}