use std::collections::HashMap;
use surge_network::network::topology::{
self as sub, BusbarSection as SubBusbar, ConnectivityNode as SubCn, TerminalConnection,
TopologyMapping,
};
use surge_network::network::{NodeBreakerTopology, SwitchDevice, SwitchType};
use super::indices::CgmesIndices;
use super::topology::SWITCH_CLASSES;
use super::types::ObjMap;
pub(crate) fn build_substation_topology(
objects: &ObjMap,
idx: &CgmesIndices,
) -> Option<NodeBreakerTopology> {
let cn_count = objects
.values()
.filter(|o| o.class == "ConnectivityNode")
.count();
if cn_count == 0 {
return None;
}
let substations: Vec<sub::Substation> = objects
.iter()
.filter(|(_, o)| o.class == "Substation")
.map(|(id, o)| sub::Substation {
id: id.clone(),
name: o.get_text("name").unwrap_or("").to_string(),
region: o.get_ref("Region").map(|s| s.to_string()),
})
.collect();
let voltage_levels: Vec<sub::VoltageLevel> = objects
.iter()
.filter(|(_, o)| o.class == "VoltageLevel")
.map(|(id, o)| {
let sub_id = o
.get_ref("Substation")
.or_else(|| o.get_ref("MemberOf_Substation"))
.unwrap_or("")
.to_string();
let bv_id = o.get_ref("BaseVoltage").unwrap_or("");
let base_kv = idx.bv_kv.get(bv_id).copied().unwrap_or(1.0);
sub::VoltageLevel {
id: id.clone(),
name: o.get_text("name").unwrap_or("").to_string(),
substation_id: sub_id,
base_kv,
}
})
.collect();
let bays: Vec<sub::Bay> = objects
.iter()
.filter(|(_, o)| o.class == "Bay")
.map(|(id, o)| sub::Bay {
id: id.clone(),
name: o.get_text("name").unwrap_or("").to_string(),
voltage_level_id: o
.get_ref("VoltageLevel")
.or_else(|| o.get_ref("MemberOf_VoltageLevel"))
.unwrap_or("")
.to_string(),
})
.collect();
let connectivity_nodes: Vec<SubCn> = objects
.iter()
.filter(|(_, o)| o.class == "ConnectivityNode")
.map(|(id, o)| SubCn {
id: id.clone(),
name: o.get_text("name").unwrap_or("").to_string(),
voltage_level_id: o
.get_ref("ConnectivityNodeContainer")
.unwrap_or("")
.to_string(),
})
.collect();
let busbar_sections: Vec<SubBusbar> = objects
.iter()
.filter(|(_, o)| o.class == "BusbarSection")
.filter_map(|(id, o)| {
let terms = idx.eq_terminals.get(id.as_str())?;
let cn_id = objects
.get(terms.first()?)?
.get_ref("ConnectivityNode")
.or_else(|| {
objects
.get(terms.first()?)?
.get_ref("TopologicalNode")
.and_then(|tn| {
connectivity_nodes
.iter()
.find(|cn| {
objects
.get(&cn.id)
.and_then(|o2| o2.get_ref("TopologicalNode"))
.is_some_and(|t| t == tn)
})
.map(|cn| cn.id.as_str())
})
})?
.to_string();
Some(SubBusbar {
id: id.clone(),
name: o.get_text("name").unwrap_or("").to_string(),
connectivity_node_id: cn_id,
ip_max: o.parse_f64("ipMax"),
})
})
.collect();
let terminal_to_cn: HashMap<&str, &str> = objects
.iter()
.filter(|(_, o)| o.class == "Terminal")
.filter_map(|(id, o)| o.get_ref("ConnectivityNode").map(|cn| (id.as_str(), cn)))
.collect();
let switches: Vec<SwitchDevice> = objects
.iter()
.filter(|(_, o)| SWITCH_CLASSES.contains(&o.class.as_str()))
.filter_map(|(id, o)| {
let terms = idx.eq_terminals.get(id.as_str())?;
if terms.len() < 2 {
return None;
}
let cn1 = terminal_to_cn.get(terms[0].as_str())?.to_string();
let cn2 = terminal_to_cn.get(terms[1].as_str())?.to_string();
let open = o
.get_text("open")
.or_else(|| o.get_text("normalOpen"))
.is_some_and(|s| s.eq_ignore_ascii_case("true"));
let normal_open = o
.get_text("normalOpen")
.is_some_and(|s| s.eq_ignore_ascii_case("true"));
let retained = o
.get_text("retained")
.is_some_and(|s| s.eq_ignore_ascii_case("true"));
let switch_type = match o.class.as_str() {
"Breaker" => SwitchType::Breaker,
"Disconnector" => SwitchType::Disconnector,
"LoadBreakSwitch" => SwitchType::LoadBreakSwitch,
"Fuse" => SwitchType::Fuse,
"GroundDisconnector" => SwitchType::GroundDisconnector,
_ => SwitchType::Switch,
};
Some(SwitchDevice {
id: id.clone(),
name: o.get_text("name").unwrap_or("").to_string(),
switch_type,
cn1_id: cn1,
cn2_id: cn2,
open,
normal_open,
retained,
rated_current: o.parse_f64("ratedCurrent"),
})
})
.collect();
let terminal_connections: Vec<TerminalConnection> = objects
.iter()
.filter(|(_, o)| o.class == "Terminal")
.filter_map(|(tid, t)| {
let cn_id = t.get_ref("ConnectivityNode")?;
let eq_id = t.get_ref("ConductingEquipment")?;
let eq_class = objects.get(eq_id).map(|o| o.class.as_str()).unwrap_or("");
if SWITCH_CLASSES.contains(&eq_class) {
return None;
}
let seq = t
.get_text("sequenceNumber")
.and_then(|s| s.parse::<u32>().ok())
.unwrap_or(1);
Some(TerminalConnection {
terminal_id: tid.clone(),
equipment_id: eq_id.to_string(),
equipment_class: eq_class.to_string(),
sequence_number: seq,
connectivity_node_id: cn_id.to_string(),
})
})
.collect();
tracing::debug!(
substations = substations.len(),
voltage_levels = voltage_levels.len(),
bays = bays.len(),
connectivity_nodes = connectivity_nodes.len(),
busbar_sections = busbar_sections.len(),
switches = switches.len(),
terminal_connections = terminal_connections.len(),
"extracted NodeBreakerTopology from CGMES"
);
Some(NodeBreakerTopology::new(
substations,
voltage_levels,
bays,
connectivity_nodes,
busbar_sections,
switches,
terminal_connections,
))
}
pub(crate) fn build_topology_mapping(
objects: &ObjMap,
idx: &CgmesIndices,
sm: &NodeBreakerTopology,
) -> TopologyMapping {
let mut connectivity_node_to_bus: HashMap<String, u32> = HashMap::new();
let mut bus_to_connectivity_nodes: HashMap<u32, Vec<String>> = HashMap::new();
let mut cn_terminal_index: HashMap<&str, &str> = HashMap::new();
for (tid, obj) in objects.iter() {
if obj.class == "Terminal"
&& let Some(cn_ref) = obj.get_ref("ConnectivityNode")
{
cn_terminal_index.entry(cn_ref).or_insert(tid);
}
}
for cn in &sm.connectivity_nodes {
let bus_num = cn_terminal_index
.get(cn.id.as_str())
.and_then(|tid| idx.terminal_tn(objects, tid))
.and_then(|tn| idx.tn_bus(tn));
if let Some(bus) = bus_num {
connectivity_node_to_bus.insert(cn.id.clone(), bus);
bus_to_connectivity_nodes
.entry(bus)
.or_default()
.push(cn.id.clone());
}
}
let consumed_switch_ids: Vec<String> = sm
.switches
.iter()
.filter(|sw| !sw.open && !sw.retained)
.filter(|sw| {
connectivity_node_to_bus.get(&sw.cn1_id) == connectivity_node_to_bus.get(&sw.cn2_id)
&& connectivity_node_to_bus.contains_key(&sw.cn1_id)
})
.map(|sw| sw.id.clone())
.collect();
let connected_cns: std::collections::HashSet<&str> = sm
.terminal_connections
.iter()
.map(|tc| tc.connectivity_node_id.as_str())
.collect();
let isolated_connectivity_node_ids: Vec<String> = sm
.connectivity_nodes
.iter()
.filter(|cn| !connected_cns.contains(cn.id.as_str()))
.map(|cn| cn.id.clone())
.collect();
TopologyMapping {
connectivity_node_to_bus,
bus_to_connectivity_nodes,
consumed_switch_ids,
isolated_connectivity_node_ids,
}
}