use super::types::{Bus, BusType};
use super::network::{
classify, connected_components, effective_injection, mark_unreferenced_islands,
power_injections, Classified, Verdict, YBusSparse,
};
use super::jacobian::JacobianPattern;
use super::sparse::RealSparseSystem;
use super::block_sparse::{BlockLu, BlockMatrix, BlockSymbolic};
#[cfg(feature = "klu")]
use super::sparse_klu::KluRealSystem;
use super::klu_native::KluNativeSystem;
#[cfg(feature = "pardiso")]
use super::sparse_pardiso::PardisoRealSystem;
pub trait LinearSolver: Sized {
fn new(n: usize, entries: &[(usize, usize, f64)]) -> Option<Self>;
fn factor_and_solve(&mut self, entries: &[(usize, usize, f64)], rhs: &[f64]) -> Option<Vec<f64>>;
fn factor_and_solve_values(&mut self, values: &[f64], rhs: &[f64]) -> Option<Vec<f64>>;
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum JacobianBackend {
Scalar,
Block,
#[cfg(feature = "klu")]
Klu,
KluNative,
#[cfg(feature = "pardiso")]
Pardiso,
}
#[derive(Clone, Debug, PartialEq)]
pub struct SolveStats {
pub status: SolveStatus,
pub mismatch_history: Vec<f64>,
pub q_limit_switches: Vec<usize>,
pub q_limit_stabilized: bool,
}
impl SolveStats {
fn from_loop(status: SolveStatus, mismatch_history: Vec<f64>) -> Self {
Self {
status,
mismatch_history,
q_limit_switches: Vec::new(),
q_limit_stabilized: true,
}
}
pub fn iterations(&self) -> usize {
self.mismatch_history.len()
}
pub fn final_mismatch(&self) -> f64 {
self.mismatch_history.last().copied().unwrap_or(0.0)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SolveStatus {
Converged,
MaxIterationsReached,
Singular,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum IslandStatus {
Converged,
MaxIterationsReached,
Singular,
NoReferenceBus,
AmbiguousReferenceBus,
}
#[derive(Debug)]
pub struct IslandReport {
pub bus_indices: Vec<usize>,
pub slack_indices: Vec<usize>,
pub status: IslandStatus,
}
fn resolve_pending(
pending: Vec<(Vec<usize>, Vec<usize>)>,
buses: &[Bus],
ybus: &YBusSparse,
overall: SolveStatus,
tol: f64,
) -> Vec<IslandReport> {
let (p_calc, q_calc) = power_injections(buses, ybus);
pending
.into_iter()
.map(|(bus_indices, slack_indices)| {
let mut max_mis = 0.0f64;
for &i in &bus_indices {
if buses[i].bus_type == BusType::Slack {
continue;
}
let (p_eff, q_eff) = effective_injection(&buses[i]);
max_mis = max_mis.max((p_eff - p_calc[i]).abs());
if buses[i].bus_type == BusType::PQ {
max_mis = max_mis.max((q_eff - q_calc[i]).abs());
}
}
let status = if max_mis < tol {
IslandStatus::Converged
} else if overall == SolveStatus::Singular {
IslandStatus::Singular
} else {
IslandStatus::MaxIterationsReached
};
IslandReport { bus_indices, slack_indices, status }
})
.collect()
}
pub(crate) fn finish_island_reports(
classified: Vec<Classified>,
buses: &[Bus],
ybus: &YBusSparse,
overall: SolveStatus,
tol: f64,
) -> Vec<IslandReport> {
let mut reports: Vec<Option<IslandReport>> = Vec::with_capacity(classified.len());
let mut pending: Vec<(usize, Vec<usize>, Vec<usize>)> = Vec::new();
for (pos, c) in classified.into_iter().enumerate() {
match c.verdict {
Verdict::NoReferenceBus => reports.push(Some(IslandReport {
bus_indices: c.bus_indices,
slack_indices: c.slack_indices,
status: IslandStatus::NoReferenceBus,
})),
Verdict::AmbiguousReferenceBus => reports.push(Some(IslandReport {
bus_indices: c.bus_indices,
slack_indices: c.slack_indices,
status: IslandStatus::AmbiguousReferenceBus,
})),
Verdict::Solvable => {
pending.push((pos, c.bus_indices, c.slack_indices));
reports.push(None);
}
}
}
let (positions, pending): (Vec<usize>, Vec<(Vec<usize>, Vec<usize>)>) = pending
.into_iter()
.map(|(pos, bus_indices, slack_indices)| (pos, (bus_indices, slack_indices)))
.unzip();
let resolved = resolve_pending(pending, buses, ybus, overall, tol);
for (pos, report) in positions.into_iter().zip(resolved) {
reports[pos] = Some(report);
}
reports.into_iter().map(|r| r.expect("every component classified exactly once")).collect()
}
pub fn newton_raphson(buses: &mut [Bus], ybus: &YBusSparse, tol: f64, max_iter: usize) -> Vec<IslandReport> {
newton_raphson_with_backend(buses, ybus, tol, max_iter, JacobianBackend::Scalar)
}
pub fn newton_raphson_with_backend(
buses: &mut [Bus],
ybus: &YBusSparse,
tol: f64,
max_iter: usize,
backend: JacobianBackend,
) -> Vec<IslandReport> {
let components = connected_components(ybus);
let classified = classify(buses, &components);
mark_unreferenced_islands(buses, &classified);
let stats = match backend {
JacobianBackend::Scalar => {
newton_raphson_cached(buses, ybus, tol, max_iter, &mut None::<RealSparseSystem>, &mut None)
}
JacobianBackend::Block => {
newton_raphson_block_cached(buses, ybus, tol, max_iter, &mut None)
}
#[cfg(feature = "klu")]
JacobianBackend::Klu => {
newton_raphson_cached(buses, ybus, tol, max_iter, &mut None::<KluRealSystem>, &mut None)
}
JacobianBackend::KluNative => {
newton_raphson_cached(buses, ybus, tol, max_iter, &mut None::<KluNativeSystem>, &mut None)
}
#[cfg(feature = "pardiso")]
JacobianBackend::Pardiso => {
newton_raphson_cached(buses, ybus, tol, max_iter, &mut None::<PardisoRealSystem>, &mut None)
}
};
finish_island_reports(classified, buses, ybus, stats.status, tol)
}
pub struct PersistentSolver {
backend: JacobianBackend,
jacobian: Option<JacobianPattern>,
scalar: Option<RealSparseSystem>,
block: Option<BlockSymbolic>,
#[cfg(feature = "klu")]
klu: Option<KluRealSystem>,
klu_native: Option<KluNativeSystem>,
#[cfg(feature = "pardiso")]
pardiso: Option<PardisoRealSystem>,
}
impl PersistentSolver {
pub fn new(backend: JacobianBackend) -> Self {
Self {
backend,
jacobian: None,
scalar: None,
block: None,
#[cfg(feature = "klu")]
klu: None,
klu_native: None,
#[cfg(feature = "pardiso")]
pardiso: None,
}
}
pub fn solve(&mut self, buses: &mut [Bus], ybus: &YBusSparse, tol: f64, max_iter: usize) -> Vec<IslandReport> {
self.solve_with_stats(buses, ybus, tol, max_iter).0
}
pub fn solve_with_stats(
&mut self,
buses: &mut [Bus],
ybus: &YBusSparse,
tol: f64,
max_iter: usize,
) -> (Vec<IslandReport>, SolveStats) {
let components = connected_components(ybus);
let classified = classify(buses, &components);
mark_unreferenced_islands(buses, &classified);
let stats = match self.backend {
JacobianBackend::Scalar => {
newton_raphson_cached(buses, ybus, tol, max_iter, &mut self.scalar, &mut self.jacobian)
}
JacobianBackend::Block => newton_raphson_block_cached(buses, ybus, tol, max_iter, &mut self.block),
#[cfg(feature = "klu")]
JacobianBackend::Klu => {
newton_raphson_cached(buses, ybus, tol, max_iter, &mut self.klu, &mut self.jacobian)
}
JacobianBackend::KluNative => {
newton_raphson_cached(buses, ybus, tol, max_iter, &mut self.klu_native, &mut self.jacobian)
}
#[cfg(feature = "pardiso")]
JacobianBackend::Pardiso => {
newton_raphson_cached(buses, ybus, tol, max_iter, &mut self.pardiso, &mut self.jacobian)
}
};
let reports = finish_island_reports(classified, buses, ybus, stats.status, tol);
(reports, stats)
}
pub fn reset(&mut self) {
self.jacobian = None;
self.scalar = None;
self.block = None;
#[cfg(feature = "klu")]
{
self.klu = None;
}
self.klu_native = None;
#[cfg(feature = "pardiso")]
{
self.pardiso = None;
}
}
}
pub fn newton_raphson_enforcing_q_limits(
buses: &mut [Bus],
ybus: &YBusSparse,
tol: f64,
max_iter: usize,
backend: JacobianBackend,
max_outer_iter: usize,
) -> Vec<IslandReport> {
newton_raphson_enforcing_q_limits_with_stats(buses, ybus, tol, max_iter, backend, max_outer_iter).0
}
pub fn newton_raphson_enforcing_q_limits_with_stats(
buses: &mut [Bus],
ybus: &YBusSparse,
tol: f64,
max_iter: usize,
backend: JacobianBackend,
max_outer_iter: usize,
) -> (Vec<IslandReport>, SolveStats) {
let mut solver = PersistentSolver::new(backend);
let mut reports = Vec::new();
let mut stats = SolveStats::from_loop(SolveStatus::MaxIterationsReached, Vec::new());
let mut switches: Vec<usize> = Vec::new();
for _ in 0..max_outer_iter {
let (r, s) = solver.solve_with_stats(buses, ybus, tol, max_iter);
reports = r;
stats = s;
let all_settled = reports
.iter()
.all(|r| !matches!(r.status, IslandStatus::Singular | IslandStatus::MaxIterationsReached));
if !all_settled {
stats.q_limit_switches = switches;
return (reports, stats);
}
let (_, q_calc) = power_injections(buses, ybus);
let mut switched = false;
for b in buses.iter_mut() {
if b.bus_type != BusType::PV {
continue;
}
let q = q_calc[b.idx];
if q < b.q_min {
b.bus_type = BusType::PQ;
b.q_spec = b.q_min;
switches.push(b.idx);
switched = true;
} else if q > b.q_max {
b.bus_type = BusType::PQ;
b.q_spec = b.q_max;
switches.push(b.idx);
switched = true;
}
}
if !switched {
stats.q_limit_switches = switches;
return (reports, stats);
}
solver.reset();
}
stats.q_limit_switches = switches;
stats.q_limit_stabilized = false;
(reports, stats)
}
fn newton_raphson_cached<S: LinearSolver>(
buses: &mut [Bus],
ybus: &YBusSparse,
tol: f64,
max_iter: usize,
cache: &mut Option<S>,
pattern: &mut Option<JacobianPattern>,
) -> SolveStats {
let non_slack_idx: Vec<usize> = buses
.iter()
.filter(|b| !matches!(b.bus_type, BusType::Slack))
.map(|b| b.idx)
.collect();
let pq_idx: Vec<usize> = buses
.iter()
.filter(|b| matches!(b.bus_type, BusType::PQ))
.map(|b| b.idx)
.collect();
let n_unknowns = non_slack_idx.len() + pq_idx.len();
if pattern.is_none() {
*pattern = Some(JacobianPattern::analyze(buses, ybus));
}
let pattern = pattern.as_ref().expect("just populated above");
let mut values: Vec<f64> = Vec::with_capacity(pattern.len());
let mut history: Vec<f64> = Vec::with_capacity(max_iter);
for _ in 0..max_iter {
let (p_calc, q_calc) = power_injections(buses, ybus);
let mut mismatch = vec![0.0; n_unknowns];
let mut mis_idx = 0;
for &i in &non_slack_idx {
let (p_eff, _) = effective_injection(&buses[i]);
mismatch[mis_idx] = p_eff - p_calc[i];
mis_idx += 1;
}
for &i in &pq_idx {
let (_, q_eff) = effective_injection(&buses[i]);
mismatch[mis_idx] = q_eff - q_calc[i];
mis_idx += 1;
}
let max_mis = mismatch.iter().fold(0.0f64, |a, &b| a.max(b.abs()));
history.push(max_mis);
if max_mis < tol {
return SolveStats::from_loop(SolveStatus::Converged, history);
}
pattern.fill(buses, &p_calc, &q_calc, &mut values);
if cache.is_none() {
*cache = S::new(n_unknowns, &pattern.to_triplets(&values));
}
let Some(system) = cache.as_mut() else {
return SolveStats::from_loop(SolveStatus::Singular, history);
};
let Some(dx) = system.factor_and_solve_values(&values, &mismatch) else {
return SolveStats::from_loop(SolveStatus::Singular, history);
};
let mut dx_idx = 0;
for &i in &non_slack_idx {
buses[i].voltage_ang += dx[dx_idx];
dx_idx += 1;
}
for &i in &pq_idx {
buses[i].voltage_mag += dx[dx_idx];
dx_idx += 1;
}
}
SolveStats::from_loop(SolveStatus::MaxIterationsReached, history)
}
#[cfg(test)]
pub(crate) fn jacobian_triplets_reference(
buses: &[Bus],
ybus: &YBusSparse,
p_calc: &[f64],
q_calc: &[f64],
) -> Vec<(usize, usize, f64)> {
let non_slack_idx: Vec<usize> = buses
.iter()
.filter(|b| !matches!(b.bus_type, BusType::Slack))
.map(|b| b.idx)
.collect();
let pq_idx: Vec<usize> = buses
.iter()
.filter(|b| matches!(b.bus_type, BusType::PQ))
.map(|b| b.idx)
.collect();
let n_angle = non_slack_idx.len();
let mut non_slack_pos: Vec<Option<usize>> = vec![None; buses.len()];
for (pos, &i) in non_slack_idx.iter().enumerate() {
non_slack_pos[i] = Some(pos);
}
let mut pq_pos: Vec<Option<usize>> = vec![None; buses.len()];
for (pos, &i) in pq_idx.iter().enumerate() {
pq_pos[i] = Some(pos);
}
let cap = jacobian_triplet_capacity(ybus, &non_slack_idx, &pq_idx);
build_jacobian_triplets(
buses, ybus, &non_slack_idx, &pq_idx, &non_slack_pos, &pq_pos, n_angle, p_calc, q_calc, cap,
)
}
#[cfg(test)]
fn jacobian_triplet_capacity(ybus: &YBusSparse, non_slack_idx: &[usize], pq_idx: &[usize]) -> usize {
let non_slack_degree: usize = non_slack_idx.iter().map(|&i| ybus.row(i).len()).sum();
let pq_degree: usize = pq_idx.iter().map(|&i| ybus.row(i).len()).sum();
2 * non_slack_degree + 2 * pq_degree
}
#[cfg(test)]
#[allow(clippy::too_many_arguments)]
fn build_jacobian_triplets(
buses: &[Bus],
ybus: &YBusSparse,
non_slack_idx: &[usize],
pq_idx: &[usize],
non_slack_pos: &[Option<usize>],
pq_pos: &[Option<usize>],
n_angle: usize,
p_calc: &[f64],
q_calc: &[f64],
triplet_capacity: usize,
) -> Vec<(usize, usize, f64)> {
let vm: Vec<f64> = buses.iter().map(|b| b.voltage_mag).collect();
let va: Vec<f64> = buses.iter().map(|b| b.voltage_ang).collect();
let mut triplets = Vec::with_capacity(triplet_capacity);
for (row_idx, &i) in non_slack_idx.iter().enumerate() {
for &(k, y_ik) in ybus.row(i) {
if k == i {
triplets.push((row_idx, row_idx, -q_calc[i] - vm[i].powi(2) * y_ik.im));
if let Some(col_idx) = pq_pos[i] {
triplets.push((row_idx, n_angle + col_idx, p_calc[i] / vm[i] + vm[i] * y_ik.re));
}
continue;
}
let angle_ik = va[i] - va[k];
if let Some(col_idx) = non_slack_pos[k] {
let h_ik = vm[i] * vm[k] * (y_ik.re * angle_ik.sin() - y_ik.im * angle_ik.cos());
triplets.push((row_idx, col_idx, h_ik));
}
if let Some(col_idx) = pq_pos[k] {
let n_ik = vm[i] * (y_ik.re * angle_ik.cos() + y_ik.im * angle_ik.sin());
triplets.push((row_idx, n_angle + col_idx, n_ik));
}
}
}
for (row_idx, &i) in pq_idx.iter().enumerate() {
for &(k, y_ik) in ybus.row(i) {
if k == i {
let col_idx = non_slack_pos[i].expect("a PQ bus is always non-slack");
triplets.push((n_angle + row_idx, col_idx, p_calc[i] - vm[i].powi(2) * y_ik.re));
triplets.push((n_angle + row_idx, n_angle + row_idx, q_calc[i] / vm[i] - vm[i] * y_ik.im));
continue;
}
let angle_ik = va[i] - va[k];
if let Some(col_idx) = non_slack_pos[k] {
let m_ik = -vm[i] * vm[k] * (y_ik.re * angle_ik.cos() + y_ik.im * angle_ik.sin());
triplets.push((n_angle + row_idx, col_idx, m_ik));
}
if let Some(col_idx) = pq_pos[k] {
let l_ik = vm[i] * (y_ik.re * angle_ik.sin() - y_ik.im * angle_ik.cos());
triplets.push((n_angle + row_idx, n_angle + col_idx, l_ik));
}
}
}
triplets
}
fn newton_raphson_block_cached(
buses: &mut [Bus], ybus: &YBusSparse, tol: f64, max_iter: usize,
symbolic: &mut Option<BlockSymbolic>,
) -> SolveStats {
let non_slack_idx: Vec<usize> = buses
.iter()
.filter(|b| !matches!(b.bus_type, BusType::Slack))
.map(|b| b.idx)
.collect();
let mut block_pos: Vec<Option<usize>> = vec![None; buses.len()];
for (pos, &i) in non_slack_idx.iter().enumerate() {
block_pos[i] = Some(pos);
}
let mut history: Vec<f64> = Vec::with_capacity(max_iter);
for _ in 0..max_iter {
let (p_calc, q_calc) = power_injections(buses, ybus);
let mismatch: Vec<[f64; 2]> = non_slack_idx
.iter()
.map(|&i| {
let (p_eff, q_eff) = effective_injection(&buses[i]);
let q_mismatch = match buses[i].bus_type {
BusType::PV => 0.0,
_ => q_eff - q_calc[i],
};
[p_eff - p_calc[i], q_mismatch]
})
.collect();
let max_mis = mismatch.iter().flatten().fold(0.0f64, |a, &b| a.max(b.abs()));
history.push(max_mis);
if max_mis < tol {
return SolveStats::from_loop(SolveStatus::Converged, history);
}
let blocks = build_jacobian_blocks(buses, ybus, &non_slack_idx, &block_pos, &p_calc, &q_calc).finish();
if symbolic.is_none() {
*symbolic = Some(BlockSymbolic::analyze(&blocks));
}
let Some(lu) = BlockLu::refactor(symbolic.as_ref().unwrap(), &blocks) else {
return SolveStats::from_loop(SolveStatus::Singular, history);
};
let dx = lu.solve(&mismatch);
for (pos, &i) in non_slack_idx.iter().enumerate() {
buses[i].voltage_ang += dx[pos][0];
buses[i].voltage_mag += dx[pos][1];
}
}
SolveStats::from_loop(SolveStatus::MaxIterationsReached, history)
}
fn build_jacobian_blocks(
buses: &[Bus],
ybus: &YBusSparse,
non_slack_idx: &[usize],
block_pos: &[Option<usize>],
p_calc: &[f64],
q_calc: &[f64],
) -> BlockMatrix {
let vm: Vec<f64> = buses.iter().map(|b| b.voltage_mag).collect();
let va: Vec<f64> = buses.iter().map(|b| b.voltage_ang).collect();
let mut blocks = BlockMatrix::new(non_slack_idx.len());
for (row, &i) in non_slack_idx.iter().enumerate() {
let is_pv = matches!(buses[i].bus_type, BusType::PV);
for &(k, y_ik) in ybus.row(i) {
if k == i {
let h_ii = -q_calc[i] - vm[i].powi(2) * y_ik.im;
let n_ii = p_calc[i] / vm[i] + vm[i] * y_ik.re;
let (m_ii, l_ii) = if is_pv {
(0.0, 1.0)
} else {
(p_calc[i] - vm[i].powi(2) * y_ik.re, q_calc[i] / vm[i] - vm[i] * y_ik.im)
};
blocks.add(row, row, [[h_ii, n_ii], [m_ii, l_ii]]);
continue;
}
let Some(col) = block_pos[k] else { continue };
let angle_ik = va[i] - va[k];
let h_ik = vm[i] * vm[k] * (y_ik.re * angle_ik.sin() - y_ik.im * angle_ik.cos());
let n_ik = vm[i] * (y_ik.re * angle_ik.cos() + y_ik.im * angle_ik.sin());
let (m_ik, l_ik) = if is_pv {
(0.0, 0.0)
} else {
(
-vm[i] * vm[k] * (y_ik.re * angle_ik.cos() + y_ik.im * angle_ik.sin()),
vm[i] * (y_ik.re * angle_ik.sin() - y_ik.im * angle_ik.cos()),
)
};
blocks.add(row, col, [[h_ik, n_ik], [m_ik, l_ik]]);
}
}
blocks
}