1use std::collections::HashMap;
11
12use num_complex::Complex;
13
14use super::ScNetwork3Ph;
15use crate::network::{
16 build_ybus_3ph, connected_components, line3ph_blocks, seq_to_phase_shunt, stamp_shunts_3ph,
17 stamp_transformers_3ph, transformer3ph_blocks,
18};
19use gridoxide_sparse::sparse;
20
21use super::{Fault, FaultAdmittance, FaultType, PHASE_ANG, ShortCircuitOptions, c_factor};
22
23const ZERO: Complex<f64> = Complex::new(0.0, 0.0);
24
25#[derive(Clone, Debug, PartialEq)]
27pub enum ShortCircuitError {
28 MixedFaultTypes,
36 UnknownFaultObject(u64),
38 UnknownFaultType(i8),
39 UnknownFaultPhase(i8),
40 Singular,
43}
44
45impl std::fmt::Display for ShortCircuitError {
46 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47 match self {
48 Self::MixedFaultTypes => write!(
49 f,
50 "all faults in one short-circuit calculation must share a fault type and phase"
51 ),
52 Self::UnknownFaultObject(id) => {
53 write!(f, "fault_object {id} is not a node in this document")
54 }
55 Self::UnknownFaultType(c) => write!(f, "unknown fault_type {c}"),
56 Self::UnknownFaultPhase(c) => write!(f, "unknown fault_phase {c}"),
57 Self::Singular => write!(
58 f,
59 "the short-circuit system is singular — in the phase domain this usually \
60 means the network has no path to ground"
61 ),
62 }
63 }
64}
65
66impl std::error::Error for ShortCircuitError {}
67
68#[derive(Clone, Debug)]
70pub struct NodeResult {
71 pub u_pu: [f64; 3],
73 pub u_angle: [f64; 3],
75 pub u: [f64; 3],
78 pub energized: bool,
80}
81
82#[derive(Clone, Debug)]
84pub struct FaultResult {
85 pub id: u64,
86 pub i_f: [f64; 3],
88 pub i_f_angle: [f64; 3],
90}
91
92#[derive(Clone, Debug)]
94pub struct BranchResult {
95 pub id: u64,
97 pub energized: bool,
101 pub i_from: [f64; 3],
102 pub i_from_angle: [f64; 3],
103 pub i_to: [f64; 3],
104 pub i_to_angle: [f64; 3],
105}
106
107#[derive(Clone, Debug)]
109pub struct SourceResult {
110 pub id: u64,
111 pub i: [f64; 3],
112 pub i_angle: [f64; 3],
113}
114
115#[derive(Clone, Debug)]
117pub struct ShuntResult {
118 pub id: u64,
119 pub i: [f64; 3],
120 pub i_angle: [f64; 3],
121}
122
123#[derive(Clone, Debug)]
125pub struct ShortCircuitReport {
126 pub nodes: Vec<NodeResult>,
128 pub faults: Vec<FaultResult>,
130 pub branches: Vec<BranchResult>,
133 pub sources: Vec<SourceResult>,
134 pub shunts: Vec<ShuntResult>,
136 pub u_bus: Vec<[Complex<f64>; 3]>,
140}
141
142pub fn short_circuit(
147 net: &ScNetwork3Ph,
148 faults: &[Fault],
149 opts: ShortCircuitOptions,
150) -> Result<ShortCircuitReport, ShortCircuitError> {
151 check_faults_agree(faults)?;
152
153 let n = net.n_nodes;
154 let dim = 3 * n;
155
156 let mut ybus = build_ybus_3ph(n, &net.lines);
161 stamp_transformers_3ph(&mut ybus, &net.transformers);
162 stamp_shunts_3ph(&mut ybus, &net.shunts);
163 let mut entries = ybus.into_entries();
164 let mut rhs = vec![ZERO; dim];
165
166 let source_y: Vec<[[Complex<f64>; 3]; 3]> = net
174 .sources
175 .iter()
176 .map(|src| seq_to_phase_shunt(src.y1, src.y0))
177 .collect();
178 let source_emf: Vec<[Complex<f64>; 3]> = net
179 .sources
180 .iter()
181 .map(|src| {
182 let c = c_factor(src.u_rated, opts.scaling);
189 [0, 1, 2].map(|p| Complex::from_polar(c, src.u_ref_angle + PHASE_ANG[p]))
190 })
191 .collect();
192
193 for (s, src) in net.sources.iter().enumerate() {
194 let y = &source_y[s];
195 let u = &source_emf[s];
196 for p in 0..3 {
197 for q in 0..3 {
198 entries.push((3 * src.node + p, 3 * src.node + q, y[p][q]));
199 rhs[3 * src.node + p] += y[p][q] * u[q];
200 }
201 }
202 }
203
204 let energized = energized_nodes(net);
212 let mut faults_by_bus: HashMap<usize, Vec<usize>> = HashMap::new();
213 for (i, f) in faults.iter().enumerate() {
214 if energized[f.bus] {
215 faults_by_bus.entry(f.bus).or_default().push(i);
216 }
217 }
218 let mut bolted_count: HashMap<usize, usize> = HashMap::new();
223
224 for (&bus, fault_idx) in &faults_by_bus {
225 for &i in fault_idx {
226 let f = &faults[i];
227 match f.y_fault {
228 FaultAdmittance::Bolted => {
229 *bolted_count.entry(bus).or_insert(0) += 1;
230 stamp_bolted(&mut entries, &mut rhs, bus, f);
231 break;
234 }
235 FaultAdmittance::Finite(y) => {
236 stamp_finite(&mut entries, &mut rhs, bus, f, y);
237 }
238 }
239 }
240 }
241
242 let mut reduced_pos: Vec<Option<usize>> = vec![None; dim];
256 let mut n_reduced = 0;
257 for k in 0..n {
258 if energized[k] {
259 for p in 0..3 {
260 reduced_pos[3 * k + p] = Some(n_reduced);
261 n_reduced += 1;
262 }
263 }
264 }
265
266 let reduced_entries: Vec<(usize, usize, Complex<f64>)> = entries
267 .iter()
268 .filter_map(|&(r, c, v)| Some((reduced_pos[r]?, reduced_pos[c]?, v)))
269 .collect();
270 let reduced_rhs: Vec<Complex<f64>> = (0..dim)
271 .filter(|&i| reduced_pos[i].is_some())
272 .map(|i| rhs[i])
273 .collect();
274
275 let mut u_bus = vec![[ZERO; 3]; n];
276 if n_reduced > 0 {
277 let sol = sparse::solve_complex(n_reduced, &reduced_entries, &reduced_rhs)
278 .ok_or(ShortCircuitError::Singular)?;
279 for k in 0..n {
280 for p in 0..3 {
281 if let Some(r) = reduced_pos[3 * k + p] {
282 u_bus[k][p] = sol[r];
283 }
284 }
285 }
286 }
287
288 let mut fault_current = vec![[ZERO; 3]; faults.len()];
290 resolve_fault_currents(
291 net,
292 faults,
293 &faults_by_bus,
294 &bolted_count,
295 &source_y,
296 &source_emf,
297 &mut u_bus,
298 &mut fault_current,
299 );
300
301 Ok(assemble_report(
302 net,
303 faults,
304 &fault_current,
305 &source_y,
306 &source_emf,
307 u_bus,
308 &energized,
309 ))
310}
311
312fn check_faults_agree(faults: &[Fault]) -> Result<(), ShortCircuitError> {
315 let Some(first) = faults.first() else {
316 return Ok(());
317 };
318 if faults
319 .iter()
320 .any(|f| f.fault_type != first.fault_type || f.fault_phase != first.fault_phase)
321 {
322 return Err(ShortCircuitError::MixedFaultTypes);
323 }
324 Ok(())
325}
326
327fn zero_column(entries: &mut Vec<(usize, usize, Complex<f64>)>, col: usize) {
335 entries.retain(|&(_, c, _)| c != col);
336}
337
338fn fold_column(entries: &mut Vec<(usize, usize, Complex<f64>)>, into: usize, from: usize) {
340 let folded: Vec<(usize, usize, Complex<f64>)> = entries
341 .iter()
342 .filter(|&&(_, c, _)| c == from)
343 .map(|&(r, _, v)| (r, into, v))
344 .collect();
345 entries.extend(folded);
346 zero_column(entries, from);
347}
348
349fn stamp_bolted(
355 entries: &mut Vec<(usize, usize, Complex<f64>)>,
356 rhs: &mut [Complex<f64>],
357 bus: usize,
358 f: &Fault,
359) {
360 let (p1, p2) = f.fault_phase.indices();
361 match f.fault_type {
362 FaultType::ThreePhase => {
363 for p in 0..3 {
364 zero_column(entries, 3 * bus + p);
365 entries.push((3 * bus + p, 3 * bus + p, -Complex::new(1.0, 0.0)));
366 rhs[3 * bus + p] = ZERO;
367 }
368 }
369 FaultType::SinglePhaseToGround => {
370 let p1 = p1.expect("a single-phase fault names its phase");
371 zero_column(entries, 3 * bus + p1);
372 entries.push((3 * bus + p1, 3 * bus + p1, -Complex::new(1.0, 0.0)));
373 rhs[3 * bus + p1] = ZERO;
374 }
375 FaultType::TwoPhase => {
376 let (p1, p2) = two_phase_indices(p1, p2);
380 fold_column(entries, 3 * bus + p1, 3 * bus + p2);
381 entries.push((3 * bus + p1, 3 * bus + p2, -Complex::new(1.0, 0.0)));
382 entries.push((3 * bus + p2, 3 * bus + p2, Complex::new(1.0, 0.0)));
383 let carried = rhs[3 * bus + p1];
384 rhs[3 * bus + p2] += carried;
385 rhs[3 * bus + p1] = ZERO;
386 }
387 FaultType::TwoPhaseToGround => {
388 let (p1, p2) = two_phase_indices(p1, p2);
389 for p in [p1, p2] {
390 zero_column(entries, 3 * bus + p);
391 entries.push((3 * bus + p, 3 * bus + p, -Complex::new(1.0, 0.0)));
392 rhs[3 * bus + p] = ZERO;
393 }
394 }
395 }
396}
397
398fn stamp_finite(
402 entries: &mut Vec<(usize, usize, Complex<f64>)>,
403 rhs: &mut [Complex<f64>],
404 bus: usize,
405 f: &Fault,
406 y: Complex<f64>,
407) {
408 let (p1, p2) = f.fault_phase.indices();
409 match f.fault_type {
410 FaultType::ThreePhase => {
411 for p in 0..3 {
412 entries.push((3 * bus + p, 3 * bus + p, y));
413 }
414 }
415 FaultType::SinglePhaseToGround => {
416 let p1 = p1.expect("a single-phase fault names its phase");
417 entries.push((3 * bus + p1, 3 * bus + p1, y));
418 }
419 FaultType::TwoPhase => {
420 let (p1, p2) = two_phase_indices(p1, p2);
421 entries.push((3 * bus + p1, 3 * bus + p1, y));
422 entries.push((3 * bus + p2, 3 * bus + p2, y));
423 entries.push((3 * bus + p1, 3 * bus + p2, -y));
424 entries.push((3 * bus + p2, 3 * bus + p1, -y));
425 }
426 FaultType::TwoPhaseToGround => {
427 let (p1, p2) = two_phase_indices(p1, p2);
431 fold_column(entries, 3 * bus + p1, 3 * bus + p2);
432 entries.push((3 * bus + p1, 3 * bus + p2, -Complex::new(1.0, 0.0)));
433 entries.push((3 * bus + p2, 3 * bus + p2, Complex::new(1.0, 0.0)));
434 entries.push((3 * bus + p2, 3 * bus + p1, y));
436 let carried = rhs[3 * bus + p1];
437 rhs[3 * bus + p2] += carried;
438 rhs[3 * bus + p1] = ZERO;
439 }
440 }
441}
442
443fn two_phase_indices(p1: Option<usize>, p2: Option<usize>) -> (usize, usize) {
444 (
445 p1.expect("a two-phase fault names its first phase"),
446 p2.expect("a two-phase fault names its second phase"),
447 )
448}
449
450#[allow(clippy::too_many_arguments)]
458fn resolve_fault_currents(
459 net: &ScNetwork3Ph,
460 faults: &[Fault],
461 faults_by_bus: &HashMap<usize, Vec<usize>>,
462 bolted_count: &HashMap<usize, usize>,
463 source_y: &[[[Complex<f64>; 3]; 3]],
464 source_emf: &[[Complex<f64>; 3]],
465 u_bus: &mut [[Complex<f64>; 3]],
466 fault_current: &mut [[Complex<f64>; 3]],
467) {
468 for (&bus, fault_idx) in faults_by_bus {
469 let subtotal = u_bus[bus];
473 let n_bolted = *bolted_count.get(&bus).unwrap_or(&0);
474 let split = n_bolted as f64;
475
476 for &i in fault_idx {
477 let f = &faults[i];
478 let (p1, p2) = f.fault_phase.indices();
479 let i_f = &mut fault_current[i];
480
481 match f.y_fault {
482 FaultAdmittance::Bolted => match f.fault_type {
483 FaultType::ThreePhase => {
484 for p in 0..3 {
485 i_f[p] = -subtotal[p] / split;
486 u_bus[bus][p] = ZERO;
487 }
488 }
489 FaultType::SinglePhaseToGround => {
490 let p1 = p1.unwrap();
491 i_f[p1] = -subtotal[p1] / split;
492 u_bus[bus][p1] = ZERO;
493 }
494 FaultType::TwoPhase => {
495 let (p1, p2) = two_phase_indices(p1, p2);
496 i_f[p1] = -subtotal[p2] / split;
497 i_f[p2] = -i_f[p1];
498 u_bus[bus][p2] = u_bus[bus][p1];
499 }
500 FaultType::TwoPhaseToGround => {
501 let (p1, p2) = two_phase_indices(p1, p2);
502 i_f[p1] = -subtotal[p1] / split;
503 i_f[p2] = -subtotal[p2] / split;
504 u_bus[bus][p1] = ZERO;
505 u_bus[bus][p2] = ZERO;
506 }
507 },
508 FaultAdmittance::Finite(y) => {
509 if n_bolted > 0 {
512 continue;
513 }
514 match f.fault_type {
515 FaultType::ThreePhase => {
516 for p in 0..3 {
517 i_f[p] = y * subtotal[p];
518 }
519 }
520 FaultType::SinglePhaseToGround => {
521 let p1 = p1.unwrap();
522 i_f[p1] = y * subtotal[p1];
523 }
524 FaultType::TwoPhase => {
525 let (p1, p2) = two_phase_indices(p1, p2);
526 i_f[p1] = y * subtotal[p1] - y * subtotal[p2];
527 i_f[p2] = y * subtotal[p2] - y * subtotal[p1];
528 }
529 FaultType::TwoPhaseToGround => {
530 let (p1, p2) = two_phase_indices(p1, p2);
531 i_f[p1] = -subtotal[p2];
532 i_f[p2] = -i_f[p1] + y * subtotal[p1];
533 u_bus[bus][p2] = u_bus[bus][p1];
534 }
535 }
536 }
537 }
538 }
539
540 let mut i_source_bus = [ZERO; 3];
542 let mut i_source_inject = [ZERO; 3];
543 for (s, src) in net.sources.iter().enumerate() {
544 if src.node != bus {
545 continue;
546 }
547 let (inject, out) = source_currents(&source_y[s], &source_emf[s], &u_bus[bus]);
548 for p in 0..3 {
549 i_source_bus[p] += out[p];
550 i_source_inject[p] += inject[p];
551 }
552 }
553
554 for &i in fault_idx {
558 let f = &faults[i];
559 let (p1, p2) = f.fault_phase.indices();
560 let i_f = &mut fault_current[i];
561 match f.y_fault {
562 FaultAdmittance::Bolted => match f.fault_type {
563 FaultType::ThreePhase => {
564 for p in 0..3 {
565 i_f[p] += i_source_bus[p] / split;
566 }
567 }
568 FaultType::SinglePhaseToGround => {
569 let p1 = p1.unwrap();
570 i_f[p1] += i_source_bus[p1] / split;
571 }
572 FaultType::TwoPhase => {
573 let (p1, p2) = two_phase_indices(p1, p2);
574 i_f[p1] += i_source_inject[p1] / split;
575 i_f[p2] -= i_source_inject[p1] / split;
576 }
577 FaultType::TwoPhaseToGround => {
578 let (p1, p2) = two_phase_indices(p1, p2);
579 i_f[p1] += i_source_bus[p1];
580 i_f[p2] += i_source_bus[p2];
581 }
582 },
583 FaultAdmittance::Finite(_) => {
584 if f.fault_type == FaultType::TwoPhaseToGround && n_bolted == 0 {
585 let (p1, p2) = two_phase_indices(p1, p2);
586 let finite = fault_idx.len() as f64;
587 i_f[p1] += i_source_inject[p1] / finite;
588 i_f[p2] -= i_source_inject[p1] / finite;
589 }
590 }
591 }
592 }
593 }
594
595 }
598
599fn source_currents(
601 y: &[[Complex<f64>; 3]; 3],
602 emf: &[Complex<f64>; 3],
603 u: &[Complex<f64>; 3],
604) -> ([Complex<f64>; 3], [Complex<f64>; 3]) {
605 let mut inject = [ZERO; 3];
606 let mut out = [ZERO; 3];
607 for p in 0..3 {
608 for q in 0..3 {
609 inject[p] += y[p][q] * emf[q];
610 out[p] += y[p][q] * (emf[q] - u[q]);
611 }
612 }
613 (inject, out)
614}
615
616fn assemble_report(
617 net: &ScNetwork3Ph,
618 faults: &[Fault],
619 fault_current: &[[Complex<f64>; 3]],
620 source_y: &[[[Complex<f64>; 3]; 3]],
621 source_emf: &[[Complex<f64>; 3]],
622 u_bus: Vec<[Complex<f64>; 3]>,
623 energized: &[bool],
624) -> ShortCircuitReport {
625 let sqrt3 = 3.0_f64.sqrt();
626
627 let nodes = (0..net.n_nodes)
628 .map(|k| {
629 let u_rated = net.u_rated[k];
630 NodeResult {
631 u_pu: [0, 1, 2].map(|p| u_bus[k][p].norm()),
632 u_angle: [0, 1, 2].map(|p| u_bus[k][p].arg()),
633 u: [0, 1, 2].map(|p| u_bus[k][p].norm() * u_rated / sqrt3),
634 energized: energized[k],
635 }
636 })
637 .collect();
638
639 let i_base = |u_rated: f64| net.s_base_va / (sqrt3 * u_rated);
643
644 let fault_results = faults
645 .iter()
646 .zip(fault_current)
647 .map(|(f, i)| {
648 let base = i_base(net.u_rated[f.bus]);
649 FaultResult {
650 id: f.id,
651 i_f: [0, 1, 2].map(|p| i[p].norm() * base),
652 i_f_angle: [0, 1, 2].map(|p| i[p].arg()),
653 }
654 })
655 .collect();
656
657 let sources = net
658 .sources
659 .iter()
660 .enumerate()
661 .map(|(s, src)| {
662 let (_, out) = source_currents(&source_y[s], &source_emf[s], &u_bus[src.node]);
663 let base = i_base(src.u_rated);
664 SourceResult {
665 id: src.id,
666 i: [0, 1, 2].map(|p| out[p].norm() * base),
667 i_angle: [0, 1, 2].map(|p| out[p].arg()),
668 }
669 })
670 .collect();
671
672 let branches = branch_currents(net, &u_bus, energized, sqrt3);
673
674 let shunts = net
677 .shunts
678 .iter()
679 .zip(&net.shunt_ids)
680 .map(|(sh, &id)| {
681 let y = seq_to_phase_shunt(sh.y1, sh.y0);
682 let u = &u_bus[sh.at];
683 let i: [Complex<f64>; 3] = [0, 1, 2].map(|p| (0..3).map(|q| y[p][q] * u[q]).sum());
684 let base = i_base(net.u_rated[sh.at]);
685 ShuntResult {
686 id,
687 i: [0, 1, 2].map(|p| i[p].norm() * base),
688 i_angle: [0, 1, 2].map(|p| i[p].arg()),
689 }
690 })
691 .collect();
692
693 ShortCircuitReport {
694 nodes,
695 faults: fault_results,
696 branches,
697 sources,
698 shunts,
699 u_bus,
700 }
701}
702
703fn branch_currents(
705 net: &ScNetwork3Ph,
706 u_bus: &[[Complex<f64>; 3]],
707 energized: &[bool],
708 sqrt3: f64,
709) -> Vec<BranchResult> {
710 let mut out = Vec::with_capacity(net.lines.len() + net.transformers.len());
711
712 let push = |from: usize,
713 to: usize,
714 blocks: [[[Complex<f64>; 3]; 3]; 4],
715 out: &mut Vec<BranchResult>| {
716 let id = net.branch_ids[out.len()];
717 let [from_closed, to_closed] = net.branch_closed[out.len()];
718 let [yff, yft, ytf, ytt] = blocks;
719 let mut i_from = [ZERO; 3];
720 let mut i_to = [ZERO; 3];
721 for p in 0..3 {
722 for q in 0..3 {
723 i_from[p] += yff[p][q] * u_bus[from][q] + yft[p][q] * u_bus[to][q];
724 i_to[p] += ytf[p][q] * u_bus[from][q] + ytt[p][q] * u_bus[to][q];
725 }
726 }
727 if from == to && from_closed && to_closed {
733 i_from = i_from.map(|i| i / 2.0);
734 i_to = i_from;
735 }
736 let base_from = net.s_base_va / (sqrt3 * net.u_rated[from]);
738 let base_to = net.s_base_va / (sqrt3 * net.u_rated[to]);
739 let mut r = BranchResult {
740 id,
741 energized: (from_closed && energized[from]) || (to_closed && energized[to]),
742 i_from: [0, 1, 2].map(|p| i_from[p].norm() * base_from),
743 i_from_angle: [0, 1, 2].map(|p| i_from[p].arg()),
744 i_to: [0, 1, 2].map(|p| i_to[p].norm() * base_to),
745 i_to_angle: [0, 1, 2].map(|p| i_to[p].arg()),
746 };
747 if from == to && !from_closed {
750 std::mem::swap(&mut r.i_from, &mut r.i_to);
751 std::mem::swap(&mut r.i_from_angle, &mut r.i_to_angle);
752 }
753 out.push(r);
754 };
755
756 for ln in &net.lines {
757 push(ln.from, ln.to, line3ph_blocks(ln), &mut out);
758 }
759 for t in &net.transformers {
760 push(t.from, t.to, transformer3ph_blocks(t), &mut out);
761 }
762 out
763}
764
765fn energized_nodes(net: &ScNetwork3Ph) -> Vec<bool> {
768 let mut ybus = build_ybus_3ph(net.n_nodes, &net.lines);
769 stamp_transformers_3ph(&mut ybus, &net.transformers);
770 let finished = ybus.finish();
771 let components = connected_components(&finished);
772
773 let mut energized = vec![false; net.n_nodes];
774 for comp in &components {
775 let has_source = comp
778 .iter()
779 .any(|&row| net.sources.iter().any(|s| s.node == row / 3));
780 if has_source {
781 for &row in comp {
782 energized[row / 3] = true;
783 }
784 }
785 }
786 energized
787}