use std::collections::HashMap;
use num_complex::Complex64;
use surge_network::Network;
use surge_network::network::{TransformerConnection, TransformerData, ZeroSeqData};
use super::helpers::{ohm_to_pu, siemens_to_pu};
use super::indices::CgmesIndices;
use super::types::ObjMap;
pub(crate) fn build_short_circuit_data(
objects: &ObjMap,
idx: &CgmesIndices,
network: &mut Network,
) {
let base_mva = network.base_mva;
let eq_to_br: HashMap<String, usize> = network
.branches
.iter()
.enumerate()
.filter(|(_, br)| !br.circuit.is_empty())
.map(|(i, br)| (br.circuit.clone(), i))
.collect();
let mut bus_to_gens: HashMap<u32, Vec<usize>> = HashMap::new();
for (i, g) in network.generators.iter().enumerate() {
bus_to_gens.entry(g.bus).or_default().push(i);
}
let mut sc_gen_count = 0u32;
for (sm_id, sm) in objects
.iter()
.filter(|(_, o)| o.class == "SynchronousMachine" || o.class == "SynchronousMachineDetailed")
{
let r0 = sm.parse_f64("r0");
let x0 = sm.parse_f64("x0");
let r2 = sm.parse_f64("r2");
let x2 = sm.parse_f64("x2Subtransient");
if r0.is_none() && x0.is_none() && r2.is_none() && x2.is_none() {
continue;
}
let bus_num = idx.terminals(sm_id).iter().find_map(|tid| {
let tn = idx.terminal_tn(objects, tid)?;
idx.tn_bus(tn)
});
let Some(bus_num) = bus_num else { continue };
if let Some(gen_indices) = bus_to_gens.get(&bus_num) {
for &gi in gen_indices {
let g = &mut network.generators[gi];
if let Some(v) = r0 {
g.fault_data.get_or_insert_with(Default::default).r0_pu = Some(v);
}
if let Some(v) = x0 {
g.fault_data.get_or_insert_with(Default::default).x0_pu = Some(v);
}
if let Some(v) = r2 {
g.fault_data.get_or_insert_with(Default::default).r2_pu = Some(v);
}
if let Some(v) = x2 {
g.fault_data.get_or_insert_with(Default::default).x2_pu = Some(v);
}
sc_gen_count += 1;
}
tracing::debug!(
sm_id,
bus_num,
?r0,
?x0,
?r2,
?x2,
"SynchronousMachine SC data → Generator"
);
}
}
let mut sc_ei_count = 0u32;
for (ei_id, ei) in objects
.iter()
.filter(|(_, o)| o.class == "EquivalentInjection")
{
let r0_ohm = ei.parse_f64("r0");
let x0_ohm = ei.parse_f64("x0");
let r2_ohm = ei.parse_f64("r2");
let x2_ohm = ei.parse_f64("x2");
if r0_ohm.is_none() && x0_ohm.is_none() && r2_ohm.is_none() && x2_ohm.is_none() {
continue;
}
let bus_num = idx.terminals(ei_id).iter().find_map(|tid| {
let tn = idx.terminal_tn(objects, tid)?;
idx.tn_bus(tn)
});
let Some(bus_num) = bus_num else { continue };
let base_kv = network
.buses
.iter()
.find(|b| b.number == bus_num)
.map(|b| b.base_kv)
.unwrap_or(1.0)
.max(1e-3);
if let Some(gen_indices) = bus_to_gens.get(&bus_num) {
for &gi in gen_indices {
let g = &mut network.generators[gi];
if let Some(v) = r0_ohm {
g.fault_data.get_or_insert_with(Default::default).r0_pu =
Some(ohm_to_pu(v, base_kv, base_mva));
}
if let Some(v) = x0_ohm {
g.fault_data.get_or_insert_with(Default::default).x0_pu =
Some(ohm_to_pu(v, base_kv, base_mva));
}
if let Some(v) = r2_ohm {
g.fault_data.get_or_insert_with(Default::default).r2_pu =
Some(ohm_to_pu(v, base_kv, base_mva));
}
if let Some(v) = x2_ohm {
g.fault_data.get_or_insert_with(Default::default).x2_pu =
Some(ohm_to_pu(v, base_kv, base_mva));
}
sc_ei_count += 1;
}
tracing::debug!(
ei_id,
bus_num,
?r0_ohm,
?x0_ohm,
?r2_ohm,
?x2_ohm,
"EquivalentInjection SC data → Generator"
);
}
}
let mut sc_line_count = 0u32;
for (line_id, line) in objects.iter().filter(|(_, o)| o.class == "ACLineSegment") {
let r0_ohm = line.parse_f64("r0");
let x0_ohm = line.parse_f64("x0");
let b0_s = line.parse_f64("b0ch");
let g0_s = line.parse_f64("g0ch");
if r0_ohm.is_none() && x0_ohm.is_none() && b0_s.is_none() && g0_s.is_none() {
continue;
}
let Some(&br_idx) = eq_to_br.get(line_id.as_str()) else {
continue;
};
let from_bus = network.branches[br_idx].from_bus;
let base_kv = network
.buses
.iter()
.find(|b| b.number == from_bus)
.map(|b| b.base_kv)
.unwrap_or(1.0)
.max(1e-3);
let br = &mut network.branches[br_idx];
if r0_ohm.is_some() || x0_ohm.is_some() || b0_s.is_some() || g0_s.is_some() {
let zs = br.zero_seq.get_or_insert_with(ZeroSeqData::default);
if let Some(v) = r0_ohm {
zs.r0 = ohm_to_pu(v, base_kv, base_mva);
}
if let Some(v) = x0_ohm {
zs.x0 = ohm_to_pu(v, base_kv, base_mva);
}
if let Some(v) = b0_s {
zs.b0 = siemens_to_pu(v, base_kv, base_mva);
}
if let Some(v) = g0_s {
zs.gi0 = siemens_to_pu(v, base_kv, base_mva) * 0.5;
zs.gj0 = zs.gi0; }
}
sc_line_count += 1;
tracing::debug!(
line_id,
br_idx,
?r0_ohm,
?x0_ohm,
?b0_s,
"ACLineSegment SC data → Branch zero-seq"
);
}
let mut sc_xfmr_count = 0u32;
for (xfmr_id, _) in objects
.iter()
.filter(|(_, o)| o.class == "PowerTransformer")
{
let ends = match idx.pte_by_xfmr.get(xfmr_id.as_str()) {
Some(e) if e.len() >= 2 => e,
_ => continue,
};
let end1_id = &ends[0].1;
let end2_id = &ends[1].1;
let end1 = match objects.get(end1_id) {
Some(o) => o,
None => continue,
};
let end2 = match objects.get(end2_id) {
Some(o) => o,
None => continue,
};
let conn1 = parse_winding_connection(end1.get_text("connectionKind"));
let conn2 = parse_winding_connection(end2.get_text("connectionKind"));
let grounded1 = end1
.get_text("grounded")
.map(|s| s == "true")
.unwrap_or(false);
let grounded2 = end2
.get_text("grounded")
.map(|s| s == "true")
.unwrap_or(false);
let xfmr_conn = derive_transformer_connection(conn1, grounded1, conn2, grounded2);
let rg1 = end1.parse_f64("rground").unwrap_or(0.0);
let xg1 = end1.parse_f64("xground").unwrap_or(0.0);
let rg2 = end2.parse_f64("rground").unwrap_or(0.0);
let xg2 = end2.parse_f64("xground").unwrap_or(0.0);
let r0_ohm = end1.parse_f64("r0");
let x0_ohm = end1.parse_f64("x0");
let Some(&br_idx) = eq_to_br.get(xfmr_id.as_str()) else {
continue;
};
let from_bus = network.branches[br_idx].from_bus;
let base_kv = network
.buses
.iter()
.find(|b| b.number == from_bus)
.map(|b| b.base_kv)
.unwrap_or(1.0)
.max(1e-3);
let br = &mut network.branches[br_idx];
br.transformer_data
.get_or_insert_with(TransformerData::default)
.transformer_connection = xfmr_conn;
if r0_ohm.is_some() || x0_ohm.is_some() {
let zs = br.zero_seq.get_or_insert_with(ZeroSeqData::default);
if let Some(v) = r0_ohm {
zs.r0 = ohm_to_pu(v, base_kv, base_mva);
}
if let Some(v) = x0_ohm {
zs.x0 = ohm_to_pu(v, base_kv, base_mva);
}
}
if grounded1 && (rg1.abs() > 1e-12 || xg1.abs() > 1e-12) {
let zs = br.zero_seq.get_or_insert_with(ZeroSeqData::default);
zs.zn = Some(Complex64::new(
ohm_to_pu(rg1, base_kv, base_mva),
ohm_to_pu(xg1, base_kv, base_mva),
));
}
if grounded2
&& (rg2.abs() > 1e-12 || xg2.abs() > 1e-12)
&& br.zero_seq.as_ref().and_then(|z| z.zn).is_none()
{
let to_bus = br.to_bus;
let to_kv = network
.buses
.iter()
.find(|b| b.number == to_bus)
.map(|b| b.base_kv)
.unwrap_or(1.0)
.max(1e-3);
let zs = br.zero_seq.get_or_insert_with(ZeroSeqData::default);
zs.zn = Some(Complex64::new(
ohm_to_pu(rg2, to_kv, base_mva),
ohm_to_pu(xg2, to_kv, base_mva),
));
}
sc_xfmr_count += 1;
tracing::debug!(
xfmr_id,
br_idx,
?xfmr_conn,
?r0_ohm,
?x0_ohm,
rg1,
xg1,
"PowerTransformerEnd SC data → Branch"
);
}
let mut sc_tsi_count = 0u32;
for (tsi_id, tsi) in objects
.iter()
.filter(|(_, o)| o.class == "TransformerStarImpedance")
{
let r0_ohm = tsi.parse_f64("r0");
let x0_ohm = tsi.parse_f64("x0");
if r0_ohm.is_none() && x0_ohm.is_none() {
continue;
}
let Some(te_id) = tsi
.get_ref("TransformerEnd")
.or_else(|| tsi.get_ref("starImpedanceEnd"))
else {
continue;
};
let Some(te) = objects.get(te_id) else {
continue;
};
let Some(xfmr_id) = te.get_ref("PowerTransformer") else {
continue;
};
let Some(&br_idx) = eq_to_br.get(xfmr_id) else {
continue;
};
let from_bus = network.branches[br_idx].from_bus;
let base_kv = network
.buses
.iter()
.find(|b| b.number == from_bus)
.map(|b| b.base_kv)
.unwrap_or(1.0)
.max(1e-3);
let br = &mut network.branches[br_idx];
if r0_ohm.is_some() || x0_ohm.is_some() {
let zs = br.zero_seq.get_or_insert_with(ZeroSeqData::default);
if let Some(v) = r0_ohm {
zs.r0 = ohm_to_pu(v, base_kv, base_mva);
}
if let Some(v) = x0_ohm {
zs.x0 = ohm_to_pu(v, base_kv, base_mva);
}
}
sc_tsi_count += 1;
tracing::debug!(
tsi_id,
xfmr_id,
br_idx,
?r0_ohm,
?x0_ohm,
"TransformerStarImpedance SC data → Branch zero-seq"
);
}
let total = sc_gen_count + sc_ei_count + sc_line_count + sc_xfmr_count + sc_tsi_count;
if total > 0 {
tracing::info!(
generators = sc_gen_count,
equiv_injections = sc_ei_count,
lines = sc_line_count,
transformers = sc_xfmr_count,
star_impedances = sc_tsi_count,
"CGMES Short Circuit profile data wired into Network"
);
}
}
fn parse_winding_connection(kind: Option<&str>) -> WindingKind {
match kind {
Some(s) => {
let local = s.rsplit('.').next().unwrap_or(s);
let local = local.rsplit('#').next().unwrap_or(local);
match local {
"D" | "delta" | "Delta" => WindingKind::Delta,
"Y" | "wye" | "Wye" => WindingKind::Wye,
"Yn" | "wyeGrounded" | "WyeGrounded" | "wyeN" => WindingKind::WyeGrounded,
"Z" | "zigzag" | "Zigzag" => WindingKind::Zigzag,
"Zn" | "zigzagGrounded" | "ZigzagGrounded" | "zigzagN" => {
WindingKind::ZigzagGrounded
}
"A" | "auto" | "Auto" | "autotransformer" => WindingKind::Auto,
_ => {
tracing::debug!(
kind = s,
"Unknown WindingConnection kind; defaulting to Wye"
);
WindingKind::Wye
}
}
}
None => WindingKind::Wye,
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum WindingKind {
Wye,
WyeGrounded,
Delta,
Zigzag,
ZigzagGrounded,
Auto,
}
fn derive_transformer_connection(
kind1: WindingKind,
grounded1: bool,
kind2: WindingKind,
grounded2: bool,
) -> TransformerConnection {
let is_gnd_wye1 = kind1 == WindingKind::WyeGrounded
|| (kind1 == WindingKind::Wye && grounded1)
|| kind1 == WindingKind::ZigzagGrounded
|| (kind1 == WindingKind::Auto && grounded1);
let is_delta1 = kind1 == WindingKind::Delta;
let is_gnd_wye2 = kind2 == WindingKind::WyeGrounded
|| (kind2 == WindingKind::Wye && grounded2)
|| kind2 == WindingKind::ZigzagGrounded
|| (kind2 == WindingKind::Auto && grounded2);
let is_delta2 = kind2 == WindingKind::Delta;
match (is_gnd_wye1, is_delta1, is_gnd_wye2, is_delta2) {
(true, _, true, _) => TransformerConnection::WyeGWyeG,
(true, _, _, true) => TransformerConnection::WyeGDelta,
(_, true, true, _) => TransformerConnection::DeltaWyeG,
(_, true, _, true) => TransformerConnection::DeltaDelta,
(true, _, _, _) => TransformerConnection::WyeGWye, (_, _, true, _) => TransformerConnection::WyeGWye, _ => TransformerConnection::DeltaDelta, }
}