use std::collections::HashMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct NodeBreakerTopology {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub substations: Vec<Substation>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub voltage_levels: Vec<VoltageLevel>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub bays: Vec<Bay>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub connectivity_nodes: Vec<ConnectivityNode>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub busbar_sections: Vec<BusbarSection>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub switches: Vec<SwitchDevice>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub terminal_connections: Vec<TerminalConnection>,
#[serde(default, skip_serializing_if = "Option::is_none")]
mapping: Option<TopologyMapping>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
mapping_stale: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum TopologyMappingState {
Missing,
Current,
Stale,
}
impl NodeBreakerTopology {
#[allow(clippy::too_many_arguments)]
pub fn new(
substations: Vec<Substation>,
voltage_levels: Vec<VoltageLevel>,
bays: Vec<Bay>,
connectivity_nodes: Vec<ConnectivityNode>,
busbar_sections: Vec<BusbarSection>,
switches: Vec<SwitchDevice>,
terminal_connections: Vec<TerminalConnection>,
) -> Self {
Self {
substations,
voltage_levels,
bays,
connectivity_nodes,
busbar_sections,
switches,
terminal_connections,
mapping: None,
mapping_stale: false,
}
}
pub fn with_mapping(mut self, reduction: TopologyMapping) -> Self {
self.install_mapping(reduction);
self
}
#[doc(hidden)]
pub fn install_mapping(&mut self, reduction: TopologyMapping) {
self.mapping = Some(reduction);
self.mapping_stale = false;
}
#[doc(hidden)]
pub fn clear_mapping(&mut self) {
self.mapping = None;
self.mapping_stale = false;
}
#[doc(hidden)]
pub fn retained_mapping(&self) -> Option<&TopologyMapping> {
self.mapping.as_ref()
}
pub fn set_switch_state(&mut self, switch_id: &str, open: bool) -> bool {
if let Some(sw) = self.switches.iter_mut().find(|s| s.id == switch_id)
&& sw.open != open
{
sw.open = open;
self.mapping_stale = true;
return true;
}
false
}
pub fn is_current(&self) -> bool {
self.mapping.is_some() && !self.mapping_stale
}
pub fn status(&self) -> TopologyMappingState {
match (self.mapping.is_some(), self.mapping_stale) {
(false, _) => TopologyMappingState::Missing,
(true, false) => TopologyMappingState::Current,
(true, true) => TopologyMappingState::Stale,
}
}
pub fn current_mapping(&self) -> Option<&TopologyMapping> {
self.mapping.as_ref().filter(|_| self.is_current())
}
pub fn switch_state(&self, switch_id: &str) -> Option<bool> {
self.switches
.iter()
.find(|s| s.id == switch_id)
.map(|s| s.open)
}
pub fn switches_of_kind(&self, sw_type: SwitchType) -> Vec<&SwitchDevice> {
self.switches
.iter()
.filter(|s| s.switch_type == sw_type)
.collect()
}
pub fn bus_for_connectivity_node(&self, cn_id: &str) -> Option<u32> {
self.current_mapping()
.and_then(|m| m.connectivity_node_to_bus.get(cn_id).copied())
}
pub fn connectivity_nodes_for_bus(&self, bus_num: u32) -> Option<&Vec<String>> {
self.current_mapping()
.and_then(|m| m.bus_to_connectivity_nodes.get(&bus_num))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Substation {
pub id: String,
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub region: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VoltageLevel {
pub id: String,
pub name: String,
pub substation_id: String,
pub base_kv: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Bay {
pub id: String,
pub name: String,
pub voltage_level_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConnectivityNode {
pub id: String,
pub name: String,
pub voltage_level_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BusbarSection {
pub id: String,
pub name: String,
pub connectivity_node_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ip_max: Option<f64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum SwitchType {
Breaker,
Disconnector,
LoadBreakSwitch,
Fuse,
GroundDisconnector,
Switch,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SwitchDevice {
pub id: String,
pub name: String,
pub switch_type: SwitchType,
pub cn1_id: String,
pub cn2_id: String,
pub open: bool,
pub normal_open: bool,
#[serde(default)]
pub retained: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rated_current: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TerminalConnection {
pub terminal_id: String,
pub equipment_id: String,
pub equipment_class: String,
pub sequence_number: u32,
pub connectivity_node_id: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TopologyMapping {
pub connectivity_node_to_bus: HashMap<String, u32>,
pub bus_to_connectivity_nodes: HashMap<u32, Vec<String>>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub consumed_switch_ids: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub isolated_connectivity_node_ids: Vec<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn substation_topology_default_is_empty() {
let sm = NodeBreakerTopology::default();
assert!(sm.substations.is_empty());
assert!(sm.switches.is_empty());
assert!(sm.retained_mapping().is_none());
assert_eq!(sm.status(), TopologyMappingState::Missing);
}
#[test]
fn set_switch_state_toggle() {
let mut sm = NodeBreakerTopology::new(
Vec::new(),
Vec::new(),
Vec::new(),
Vec::new(),
Vec::new(),
vec![SwitchDevice {
id: "BRK_1".into(),
name: "Breaker 1".into(),
switch_type: SwitchType::Breaker,
cn1_id: "CN_A".into(),
cn2_id: "CN_B".into(),
open: false,
normal_open: false,
retained: false,
rated_current: None,
}],
Vec::new(),
)
.with_mapping(TopologyMapping::default());
assert!(sm.set_switch_state("BRK_1", true));
assert_eq!(sm.switch_state("BRK_1"), Some(true));
assert!(sm.retained_mapping().is_some());
assert_eq!(sm.status(), TopologyMappingState::Stale);
assert_eq!(sm.bus_for_connectivity_node("CN_A"), None);
assert!(!sm.set_switch_state("BRK_1", true));
assert!(!sm.set_switch_state("BRK_UNKNOWN", false));
}
#[test]
fn switches_of_type_filter() {
let sm = NodeBreakerTopology::new(
Vec::new(),
Vec::new(),
Vec::new(),
Vec::new(),
Vec::new(),
vec![
SwitchDevice {
id: "BRK_1".into(),
name: "B1".into(),
switch_type: SwitchType::Breaker,
cn1_id: "A".into(),
cn2_id: "B".into(),
open: false,
normal_open: false,
retained: false,
rated_current: None,
},
SwitchDevice {
id: "DIS_1".into(),
name: "D1".into(),
switch_type: SwitchType::Disconnector,
cn1_id: "B".into(),
cn2_id: "C".into(),
open: false,
normal_open: false,
retained: false,
rated_current: None,
},
],
Vec::new(),
);
assert_eq!(sm.switches_of_kind(SwitchType::Breaker).len(), 1);
assert_eq!(sm.switches_of_kind(SwitchType::Disconnector).len(), 1);
assert_eq!(sm.switches_of_kind(SwitchType::Fuse).len(), 0);
}
#[test]
fn serde_roundtrip() {
let sm = NodeBreakerTopology::new(
vec![Substation {
id: "SUB_1".into(),
name: "Station Alpha".into(),
region: Some("RGN_1".into()),
}],
vec![VoltageLevel {
id: "VL_220".into(),
name: "220 kV".into(),
substation_id: "SUB_1".into(),
base_kv: 220.0,
}],
Vec::new(),
vec![ConnectivityNode {
id: "CN_A".into(),
name: "Node A".into(),
voltage_level_id: "VL_220".into(),
}],
Vec::new(),
vec![SwitchDevice {
id: "BRK_1".into(),
name: "Breaker 1".into(),
switch_type: SwitchType::Breaker,
cn1_id: "CN_A".into(),
cn2_id: "CN_B".into(),
open: false,
normal_open: false,
retained: false,
rated_current: Some(2000.0),
}],
Vec::new(),
)
.with_mapping(TopologyMapping {
connectivity_node_to_bus: [("CN_A".into(), 1), ("CN_B".into(), 1)]
.into_iter()
.collect(),
bus_to_connectivity_nodes: [(1, vec!["CN_A".into(), "CN_B".into()])]
.into_iter()
.collect(),
consumed_switch_ids: vec!["BRK_1".into()],
isolated_connectivity_node_ids: vec![],
});
let json = serde_json::to_string(&sm).unwrap();
let deser: NodeBreakerTopology = serde_json::from_str(&json).unwrap();
assert_eq!(deser.substations.len(), 1);
assert_eq!(deser.switches.len(), 1);
assert_eq!(deser.switches[0].switch_type, SwitchType::Breaker);
assert_eq!(deser.bus_for_connectivity_node("CN_A"), Some(1));
assert_eq!(deser.connectivity_nodes_for_bus(1).unwrap().len(), 2);
assert!(deser.is_current());
assert_eq!(deser.status(), TopologyMappingState::Current);
}
#[test]
fn network_serde_without_substation_topology() {
let json = r#"{"name":"test","base_mva":100.0,"buses":[],"branches":[],"generators":[],"loads":[]}"#;
let net: crate::Network = serde_json::from_str(json).unwrap();
assert!(net.topology.is_none());
}
}