use std::collections::HashMap;
use num_complex::Complex;
use super::ScNetwork3Ph;
use crate::network::{
build_ybus_3ph, connected_components, line3ph_blocks, seq_to_phase_shunt, stamp_shunts_3ph,
stamp_transformers_3ph, transformer3ph_blocks,
};
use gridoxide_sparse::sparse;
use super::{Fault, FaultAdmittance, FaultType, PHASE_ANG, ShortCircuitOptions, c_factor};
const ZERO: Complex<f64> = Complex::new(0.0, 0.0);
#[derive(Clone, Debug, PartialEq)]
pub enum ShortCircuitError {
MixedFaultTypes,
UnknownFaultObject(u64),
UnknownFaultType(i8),
UnknownFaultPhase(i8),
Singular,
}
impl std::fmt::Display for ShortCircuitError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::MixedFaultTypes => write!(
f,
"all faults in one short-circuit calculation must share a fault type and phase"
),
Self::UnknownFaultObject(id) => {
write!(f, "fault_object {id} is not a node in this document")
}
Self::UnknownFaultType(c) => write!(f, "unknown fault_type {c}"),
Self::UnknownFaultPhase(c) => write!(f, "unknown fault_phase {c}"),
Self::Singular => write!(
f,
"the short-circuit system is singular — in the phase domain this usually \
means the network has no path to ground"
),
}
}
}
impl std::error::Error for ShortCircuitError {}
#[derive(Clone, Debug)]
pub struct NodeResult {
pub u_pu: [f64; 3],
pub u_angle: [f64; 3],
pub u: [f64; 3],
pub energized: bool,
}
#[derive(Clone, Debug)]
pub struct FaultResult {
pub id: u64,
pub i_f: [f64; 3],
pub i_f_angle: [f64; 3],
}
#[derive(Clone, Debug)]
pub struct BranchResult {
pub id: u64,
pub energized: bool,
pub i_from: [f64; 3],
pub i_from_angle: [f64; 3],
pub i_to: [f64; 3],
pub i_to_angle: [f64; 3],
}
#[derive(Clone, Debug)]
pub struct SourceResult {
pub id: u64,
pub i: [f64; 3],
pub i_angle: [f64; 3],
}
#[derive(Clone, Debug)]
pub struct ShuntResult {
pub id: u64,
pub i: [f64; 3],
pub i_angle: [f64; 3],
}
#[derive(Clone, Debug)]
pub struct ShortCircuitReport {
pub nodes: Vec<NodeResult>,
pub faults: Vec<FaultResult>,
pub branches: Vec<BranchResult>,
pub sources: Vec<SourceResult>,
pub shunts: Vec<ShuntResult>,
pub u_bus: Vec<[Complex<f64>; 3]>,
}
pub fn short_circuit(
net: &ScNetwork3Ph,
faults: &[Fault],
opts: ShortCircuitOptions,
) -> Result<ShortCircuitReport, ShortCircuitError> {
check_faults_agree(faults)?;
let n = net.n_nodes;
let dim = 3 * n;
let mut ybus = build_ybus_3ph(n, &net.lines);
stamp_transformers_3ph(&mut ybus, &net.transformers);
stamp_shunts_3ph(&mut ybus, &net.shunts);
let mut entries = ybus.into_entries();
let mut rhs = vec![ZERO; dim];
let source_y: Vec<[[Complex<f64>; 3]; 3]> = net
.sources
.iter()
.map(|src| seq_to_phase_shunt(src.y1, src.y0))
.collect();
let source_emf: Vec<[Complex<f64>; 3]> = net
.sources
.iter()
.map(|src| {
let c = c_factor(src.u_rated, opts.scaling);
[0, 1, 2].map(|p| Complex::from_polar(c, src.u_ref_angle + PHASE_ANG[p]))
})
.collect();
for (s, src) in net.sources.iter().enumerate() {
let y = &source_y[s];
let u = &source_emf[s];
for p in 0..3 {
for q in 0..3 {
entries.push((3 * src.node + p, 3 * src.node + q, y[p][q]));
rhs[3 * src.node + p] += y[p][q] * u[q];
}
}
}
let energized = energized_nodes(net);
let mut faults_by_bus: HashMap<usize, Vec<usize>> = HashMap::new();
for (i, f) in faults.iter().enumerate() {
if energized[f.bus] {
faults_by_bus.entry(f.bus).or_default().push(i);
}
}
let mut bolted_count: HashMap<usize, usize> = HashMap::new();
for (&bus, fault_idx) in &faults_by_bus {
for &i in fault_idx {
let f = &faults[i];
match f.y_fault {
FaultAdmittance::Bolted => {
*bolted_count.entry(bus).or_insert(0) += 1;
stamp_bolted(&mut entries, &mut rhs, bus, f);
break;
}
FaultAdmittance::Finite(y) => {
stamp_finite(&mut entries, &mut rhs, bus, f, y);
}
}
}
}
let mut reduced_pos: Vec<Option<usize>> = vec![None; dim];
let mut n_reduced = 0;
for k in 0..n {
if energized[k] {
for p in 0..3 {
reduced_pos[3 * k + p] = Some(n_reduced);
n_reduced += 1;
}
}
}
let reduced_entries: Vec<(usize, usize, Complex<f64>)> = entries
.iter()
.filter_map(|&(r, c, v)| Some((reduced_pos[r]?, reduced_pos[c]?, v)))
.collect();
let reduced_rhs: Vec<Complex<f64>> = (0..dim)
.filter(|&i| reduced_pos[i].is_some())
.map(|i| rhs[i])
.collect();
let mut u_bus = vec![[ZERO; 3]; n];
if n_reduced > 0 {
let sol = sparse::solve_complex(n_reduced, &reduced_entries, &reduced_rhs)
.ok_or(ShortCircuitError::Singular)?;
for k in 0..n {
for p in 0..3 {
if let Some(r) = reduced_pos[3 * k + p] {
u_bus[k][p] = sol[r];
}
}
}
}
let mut fault_current = vec![[ZERO; 3]; faults.len()];
resolve_fault_currents(
net,
faults,
&faults_by_bus,
&bolted_count,
&source_y,
&source_emf,
&mut u_bus,
&mut fault_current,
);
Ok(assemble_report(
net,
faults,
&fault_current,
&source_y,
&source_emf,
u_bus,
&energized,
))
}
fn check_faults_agree(faults: &[Fault]) -> Result<(), ShortCircuitError> {
let Some(first) = faults.first() else {
return Ok(());
};
if faults
.iter()
.any(|f| f.fault_type != first.fault_type || f.fault_phase != first.fault_phase)
{
return Err(ShortCircuitError::MixedFaultTypes);
}
Ok(())
}
fn zero_column(entries: &mut Vec<(usize, usize, Complex<f64>)>, col: usize) {
entries.retain(|&(_, c, _)| c != col);
}
fn fold_column(entries: &mut Vec<(usize, usize, Complex<f64>)>, into: usize, from: usize) {
let folded: Vec<(usize, usize, Complex<f64>)> = entries
.iter()
.filter(|&&(_, c, _)| c == from)
.map(|&(r, _, v)| (r, into, v))
.collect();
entries.extend(folded);
zero_column(entries, from);
}
fn stamp_bolted(
entries: &mut Vec<(usize, usize, Complex<f64>)>,
rhs: &mut [Complex<f64>],
bus: usize,
f: &Fault,
) {
let (p1, p2) = f.fault_phase.indices();
match f.fault_type {
FaultType::ThreePhase => {
for p in 0..3 {
zero_column(entries, 3 * bus + p);
entries.push((3 * bus + p, 3 * bus + p, -Complex::new(1.0, 0.0)));
rhs[3 * bus + p] = ZERO;
}
}
FaultType::SinglePhaseToGround => {
let p1 = p1.expect("a single-phase fault names its phase");
zero_column(entries, 3 * bus + p1);
entries.push((3 * bus + p1, 3 * bus + p1, -Complex::new(1.0, 0.0)));
rhs[3 * bus + p1] = ZERO;
}
FaultType::TwoPhase => {
let (p1, p2) = two_phase_indices(p1, p2);
fold_column(entries, 3 * bus + p1, 3 * bus + p2);
entries.push((3 * bus + p1, 3 * bus + p2, -Complex::new(1.0, 0.0)));
entries.push((3 * bus + p2, 3 * bus + p2, Complex::new(1.0, 0.0)));
let carried = rhs[3 * bus + p1];
rhs[3 * bus + p2] += carried;
rhs[3 * bus + p1] = ZERO;
}
FaultType::TwoPhaseToGround => {
let (p1, p2) = two_phase_indices(p1, p2);
for p in [p1, p2] {
zero_column(entries, 3 * bus + p);
entries.push((3 * bus + p, 3 * bus + p, -Complex::new(1.0, 0.0)));
rhs[3 * bus + p] = ZERO;
}
}
}
}
fn stamp_finite(
entries: &mut Vec<(usize, usize, Complex<f64>)>,
rhs: &mut [Complex<f64>],
bus: usize,
f: &Fault,
y: Complex<f64>,
) {
let (p1, p2) = f.fault_phase.indices();
match f.fault_type {
FaultType::ThreePhase => {
for p in 0..3 {
entries.push((3 * bus + p, 3 * bus + p, y));
}
}
FaultType::SinglePhaseToGround => {
let p1 = p1.expect("a single-phase fault names its phase");
entries.push((3 * bus + p1, 3 * bus + p1, y));
}
FaultType::TwoPhase => {
let (p1, p2) = two_phase_indices(p1, p2);
entries.push((3 * bus + p1, 3 * bus + p1, y));
entries.push((3 * bus + p2, 3 * bus + p2, y));
entries.push((3 * bus + p1, 3 * bus + p2, -y));
entries.push((3 * bus + p2, 3 * bus + p1, -y));
}
FaultType::TwoPhaseToGround => {
let (p1, p2) = two_phase_indices(p1, p2);
fold_column(entries, 3 * bus + p1, 3 * bus + p2);
entries.push((3 * bus + p1, 3 * bus + p2, -Complex::new(1.0, 0.0)));
entries.push((3 * bus + p2, 3 * bus + p2, Complex::new(1.0, 0.0)));
entries.push((3 * bus + p2, 3 * bus + p1, y));
let carried = rhs[3 * bus + p1];
rhs[3 * bus + p2] += carried;
rhs[3 * bus + p1] = ZERO;
}
}
}
fn two_phase_indices(p1: Option<usize>, p2: Option<usize>) -> (usize, usize) {
(
p1.expect("a two-phase fault names its first phase"),
p2.expect("a two-phase fault names its second phase"),
)
}
#[allow(clippy::too_many_arguments)]
fn resolve_fault_currents(
net: &ScNetwork3Ph,
faults: &[Fault],
faults_by_bus: &HashMap<usize, Vec<usize>>,
bolted_count: &HashMap<usize, usize>,
source_y: &[[[Complex<f64>; 3]; 3]],
source_emf: &[[Complex<f64>; 3]],
u_bus: &mut [[Complex<f64>; 3]],
fault_current: &mut [[Complex<f64>; 3]],
) {
for (&bus, fault_idx) in faults_by_bus {
let subtotal = u_bus[bus];
let n_bolted = *bolted_count.get(&bus).unwrap_or(&0);
let split = n_bolted as f64;
for &i in fault_idx {
let f = &faults[i];
let (p1, p2) = f.fault_phase.indices();
let i_f = &mut fault_current[i];
match f.y_fault {
FaultAdmittance::Bolted => match f.fault_type {
FaultType::ThreePhase => {
for p in 0..3 {
i_f[p] = -subtotal[p] / split;
u_bus[bus][p] = ZERO;
}
}
FaultType::SinglePhaseToGround => {
let p1 = p1.unwrap();
i_f[p1] = -subtotal[p1] / split;
u_bus[bus][p1] = ZERO;
}
FaultType::TwoPhase => {
let (p1, p2) = two_phase_indices(p1, p2);
i_f[p1] = -subtotal[p2] / split;
i_f[p2] = -i_f[p1];
u_bus[bus][p2] = u_bus[bus][p1];
}
FaultType::TwoPhaseToGround => {
let (p1, p2) = two_phase_indices(p1, p2);
i_f[p1] = -subtotal[p1] / split;
i_f[p2] = -subtotal[p2] / split;
u_bus[bus][p1] = ZERO;
u_bus[bus][p2] = ZERO;
}
},
FaultAdmittance::Finite(y) => {
if n_bolted > 0 {
continue;
}
match f.fault_type {
FaultType::ThreePhase => {
for p in 0..3 {
i_f[p] = y * subtotal[p];
}
}
FaultType::SinglePhaseToGround => {
let p1 = p1.unwrap();
i_f[p1] = y * subtotal[p1];
}
FaultType::TwoPhase => {
let (p1, p2) = two_phase_indices(p1, p2);
i_f[p1] = y * subtotal[p1] - y * subtotal[p2];
i_f[p2] = y * subtotal[p2] - y * subtotal[p1];
}
FaultType::TwoPhaseToGround => {
let (p1, p2) = two_phase_indices(p1, p2);
i_f[p1] = -subtotal[p2];
i_f[p2] = -i_f[p1] + y * subtotal[p1];
u_bus[bus][p2] = u_bus[bus][p1];
}
}
}
}
}
let mut i_source_bus = [ZERO; 3];
let mut i_source_inject = [ZERO; 3];
for (s, src) in net.sources.iter().enumerate() {
if src.node != bus {
continue;
}
let (inject, out) = source_currents(&source_y[s], &source_emf[s], &u_bus[bus]);
for p in 0..3 {
i_source_bus[p] += out[p];
i_source_inject[p] += inject[p];
}
}
for &i in fault_idx {
let f = &faults[i];
let (p1, p2) = f.fault_phase.indices();
let i_f = &mut fault_current[i];
match f.y_fault {
FaultAdmittance::Bolted => match f.fault_type {
FaultType::ThreePhase => {
for p in 0..3 {
i_f[p] += i_source_bus[p] / split;
}
}
FaultType::SinglePhaseToGround => {
let p1 = p1.unwrap();
i_f[p1] += i_source_bus[p1] / split;
}
FaultType::TwoPhase => {
let (p1, p2) = two_phase_indices(p1, p2);
i_f[p1] += i_source_inject[p1] / split;
i_f[p2] -= i_source_inject[p1] / split;
}
FaultType::TwoPhaseToGround => {
let (p1, p2) = two_phase_indices(p1, p2);
i_f[p1] += i_source_bus[p1];
i_f[p2] += i_source_bus[p2];
}
},
FaultAdmittance::Finite(_) => {
if f.fault_type == FaultType::TwoPhaseToGround && n_bolted == 0 {
let (p1, p2) = two_phase_indices(p1, p2);
let finite = fault_idx.len() as f64;
i_f[p1] += i_source_inject[p1] / finite;
i_f[p2] -= i_source_inject[p1] / finite;
}
}
}
}
}
}
fn source_currents(
y: &[[Complex<f64>; 3]; 3],
emf: &[Complex<f64>; 3],
u: &[Complex<f64>; 3],
) -> ([Complex<f64>; 3], [Complex<f64>; 3]) {
let mut inject = [ZERO; 3];
let mut out = [ZERO; 3];
for p in 0..3 {
for q in 0..3 {
inject[p] += y[p][q] * emf[q];
out[p] += y[p][q] * (emf[q] - u[q]);
}
}
(inject, out)
}
fn assemble_report(
net: &ScNetwork3Ph,
faults: &[Fault],
fault_current: &[[Complex<f64>; 3]],
source_y: &[[[Complex<f64>; 3]; 3]],
source_emf: &[[Complex<f64>; 3]],
u_bus: Vec<[Complex<f64>; 3]>,
energized: &[bool],
) -> ShortCircuitReport {
let sqrt3 = 3.0_f64.sqrt();
let nodes = (0..net.n_nodes)
.map(|k| {
let u_rated = net.u_rated[k];
NodeResult {
u_pu: [0, 1, 2].map(|p| u_bus[k][p].norm()),
u_angle: [0, 1, 2].map(|p| u_bus[k][p].arg()),
u: [0, 1, 2].map(|p| u_bus[k][p].norm() * u_rated / sqrt3),
energized: energized[k],
}
})
.collect();
let i_base = |u_rated: f64| net.s_base_va / (sqrt3 * u_rated);
let fault_results = faults
.iter()
.zip(fault_current)
.map(|(f, i)| {
let base = i_base(net.u_rated[f.bus]);
FaultResult {
id: f.id,
i_f: [0, 1, 2].map(|p| i[p].norm() * base),
i_f_angle: [0, 1, 2].map(|p| i[p].arg()),
}
})
.collect();
let sources = net
.sources
.iter()
.enumerate()
.map(|(s, src)| {
let (_, out) = source_currents(&source_y[s], &source_emf[s], &u_bus[src.node]);
let base = i_base(src.u_rated);
SourceResult {
id: src.id,
i: [0, 1, 2].map(|p| out[p].norm() * base),
i_angle: [0, 1, 2].map(|p| out[p].arg()),
}
})
.collect();
let branches = branch_currents(net, &u_bus, energized, sqrt3);
let shunts = net
.shunts
.iter()
.zip(&net.shunt_ids)
.map(|(sh, &id)| {
let y = seq_to_phase_shunt(sh.y1, sh.y0);
let u = &u_bus[sh.at];
let i: [Complex<f64>; 3] = [0, 1, 2].map(|p| (0..3).map(|q| y[p][q] * u[q]).sum());
let base = i_base(net.u_rated[sh.at]);
ShuntResult {
id,
i: [0, 1, 2].map(|p| i[p].norm() * base),
i_angle: [0, 1, 2].map(|p| i[p].arg()),
}
})
.collect();
ShortCircuitReport {
nodes,
faults: fault_results,
branches,
sources,
shunts,
u_bus,
}
}
fn branch_currents(
net: &ScNetwork3Ph,
u_bus: &[[Complex<f64>; 3]],
energized: &[bool],
sqrt3: f64,
) -> Vec<BranchResult> {
let mut out = Vec::with_capacity(net.lines.len() + net.transformers.len());
let push = |from: usize,
to: usize,
blocks: [[[Complex<f64>; 3]; 3]; 4],
out: &mut Vec<BranchResult>| {
let id = net.branch_ids[out.len()];
let [from_closed, to_closed] = net.branch_closed[out.len()];
let [yff, yft, ytf, ytt] = blocks;
let mut i_from = [ZERO; 3];
let mut i_to = [ZERO; 3];
for p in 0..3 {
for q in 0..3 {
i_from[p] += yff[p][q] * u_bus[from][q] + yft[p][q] * u_bus[to][q];
i_to[p] += ytf[p][q] * u_bus[from][q] + ytt[p][q] * u_bus[to][q];
}
}
if from == to && from_closed && to_closed {
i_from = i_from.map(|i| i / 2.0);
i_to = i_from;
}
let base_from = net.s_base_va / (sqrt3 * net.u_rated[from]);
let base_to = net.s_base_va / (sqrt3 * net.u_rated[to]);
let mut r = BranchResult {
id,
energized: (from_closed && energized[from]) || (to_closed && energized[to]),
i_from: [0, 1, 2].map(|p| i_from[p].norm() * base_from),
i_from_angle: [0, 1, 2].map(|p| i_from[p].arg()),
i_to: [0, 1, 2].map(|p| i_to[p].norm() * base_to),
i_to_angle: [0, 1, 2].map(|p| i_to[p].arg()),
};
if from == to && !from_closed {
std::mem::swap(&mut r.i_from, &mut r.i_to);
std::mem::swap(&mut r.i_from_angle, &mut r.i_to_angle);
}
out.push(r);
};
for ln in &net.lines {
push(ln.from, ln.to, line3ph_blocks(ln), &mut out);
}
for t in &net.transformers {
push(t.from, t.to, transformer3ph_blocks(t), &mut out);
}
out
}
fn energized_nodes(net: &ScNetwork3Ph) -> Vec<bool> {
let mut ybus = build_ybus_3ph(net.n_nodes, &net.lines);
stamp_transformers_3ph(&mut ybus, &net.transformers);
let finished = ybus.finish();
let components = connected_components(&finished);
let mut energized = vec![false; net.n_nodes];
for comp in &components {
let has_source = comp
.iter()
.any(|&row| net.sources.iter().any(|s| s.node == row / 3));
if has_source {
for &row in comp {
energized[row / 3] = true;
}
}
}
energized
}