use super::jacobian::JacobianPattern;
use super::network::{
BusVoltages, Classified, Verdict, YBusSparse, classify, connected_components,
effective_injection, mark_unreferenced_islands, power_injections, power_injections_from,
};
use gridoxide_sparse::block_sparse::{BlockLu, BlockMatrix, BlockSymbolic};
use super::types::{Bus, BusType};
use gridoxide_sparse::lu::{LuBackend, SparseLu};
pub use gridoxide_sparse::sparse::LinearSolver;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum JacobianBackend {
Scalar,
Block,
#[cfg(feature = "klu")]
Klu,
#[default]
KluNative,
#[cfg(feature = "pardiso")]
Pardiso,
}
impl JacobianBackend {
pub fn lu_backend(self) -> LuBackend {
match self {
JacobianBackend::Scalar | JacobianBackend::Block => LuBackend::Faer,
JacobianBackend::KluNative => LuBackend::KluNative,
#[cfg(feature = "klu")]
JacobianBackend::Klu => LuBackend::Klu,
#[cfg(feature = "pardiso")]
JacobianBackend::Pardiso => LuBackend::Pardiso,
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct SolveStats {
pub status: SolveStatus,
pub mismatch_history: Vec<f64>,
}
impl SolveStats {
pub fn from_loop(status: SolveStatus, mismatch_history: Vec<f64>) -> Self {
Self {
status,
mismatch_history,
}
}
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(Clone, 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::default())
}
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::Block => {
newton_raphson_block_cached(buses, ybus, tol, max_iter, &mut None)
}
_ => newton_raphson_cached(
buses,
ybus,
tol,
max_iter,
&mut SparseLu::new(backend.lu_backend()),
&mut None,
),
};
finish_island_reports(classified, buses, ybus, stats.status, tol)
}
pub struct PersistentSolver {
backend: JacobianBackend,
jacobian: Option<JacobianPattern>,
components: Option<Vec<Vec<usize>>>,
lu: SparseLu,
block: Option<BlockSymbolic>,
}
impl PersistentSolver {
pub fn new(backend: JacobianBackend) -> Self {
Self {
backend,
jacobian: None,
components: None,
lu: SparseLu::new(backend.lu_backend()),
block: 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) {
if self
.components
.as_ref()
.is_none_or(|c| c.iter().map(Vec::len).sum::<usize>() != buses.len())
{
self.components = Some(connected_components(ybus));
}
let components = self.components.as_ref().expect("just populated above");
let classified = classify(buses, components);
mark_unreferenced_islands(buses, &classified);
let stats = match self.backend {
JacobianBackend::Block => {
newton_raphson_block_cached(buses, ybus, tol, max_iter, &mut self.block)
}
_ => {
newton_raphson_cached(buses, ybus, tol, max_iter, &mut self.lu, &mut self.jacobian)
}
};
let reports = finish_island_reports(classified, buses, ybus, stats.status, tol);
(reports, stats)
}
pub fn invalidate_admittances(&mut self) {
self.jacobian = None;
}
pub fn reset(&mut self) {
self.jacobian = None;
self.components = None;
self.lu.reset();
self.block = None;
}
}
fn newton_raphson_cached(
buses: &mut [Bus],
ybus: &YBusSparse,
tol: f64,
max_iter: usize,
lu: &mut SparseLu,
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() {
let mut fresh = JacobianPattern::analyze(buses, ybus);
if let Some(order) = lu.storage_order() {
fresh.permute(order);
}
*pattern = Some(fresh);
}
let pattern = pattern.as_mut().expect("just populated above");
let mut values: Vec<f64> = Vec::with_capacity(pattern.len());
let mut history: Vec<f64> = Vec::with_capacity(max_iter);
let mut voltages = BusVoltages::default();
for _ in 0..max_iter {
voltages.update(buses);
let (p_calc, q_calc) = power_injections_from(&voltages, 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_with(&voltages, &p_calc, &q_calc, &mut values);
if !lu.is_analyzed() {
if !lu.analyze(n_unknowns, &pattern.to_triplets(&values)) {
return SolveStats::from_loop(SolveStatus::Singular, history);
}
if let Some(order) = lu.adopt_storage_order() {
pattern.permute(&order);
values = order.iter().map(|&i| values[i]).collect();
}
}
let Some(dx) = lu.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)
}
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
}