1use std::collections::HashMap;
13
14use serde::{Deserialize, Serialize};
15
16#[derive(Debug, Clone, Default, Serialize, Deserialize)]
30pub struct NodeBreakerTopology {
31 #[serde(default, skip_serializing_if = "Vec::is_empty")]
33 pub substations: Vec<Substation>,
34
35 #[serde(default, skip_serializing_if = "Vec::is_empty")]
37 pub voltage_levels: Vec<VoltageLevel>,
38
39 #[serde(default, skip_serializing_if = "Vec::is_empty")]
41 pub bays: Vec<Bay>,
42
43 #[serde(default, skip_serializing_if = "Vec::is_empty")]
45 pub connectivity_nodes: Vec<ConnectivityNode>,
46
47 #[serde(default, skip_serializing_if = "Vec::is_empty")]
49 pub busbar_sections: Vec<BusbarSection>,
50
51 #[serde(default, skip_serializing_if = "Vec::is_empty")]
53 pub switches: Vec<SwitchDevice>,
54
55 #[serde(default, skip_serializing_if = "Vec::is_empty")]
57 pub terminal_connections: Vec<TerminalConnection>,
58
59 #[serde(default, skip_serializing_if = "Option::is_none")]
62 mapping: Option<TopologyMapping>,
63
64 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
72 mapping_stale: bool,
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
77pub enum TopologyMappingState {
78 Missing,
79 Current,
80 Stale,
81}
82
83impl NodeBreakerTopology {
84 #[allow(clippy::too_many_arguments)]
86 pub fn new(
87 substations: Vec<Substation>,
88 voltage_levels: Vec<VoltageLevel>,
89 bays: Vec<Bay>,
90 connectivity_nodes: Vec<ConnectivityNode>,
91 busbar_sections: Vec<BusbarSection>,
92 switches: Vec<SwitchDevice>,
93 terminal_connections: Vec<TerminalConnection>,
94 ) -> Self {
95 Self {
96 substations,
97 voltage_levels,
98 bays,
99 connectivity_nodes,
100 busbar_sections,
101 switches,
102 terminal_connections,
103 mapping: None,
104 mapping_stale: false,
105 }
106 }
107
108 pub fn with_mapping(mut self, reduction: TopologyMapping) -> Self {
110 self.install_mapping(reduction);
111 self
112 }
113
114 #[doc(hidden)]
116 pub fn install_mapping(&mut self, reduction: TopologyMapping) {
117 self.mapping = Some(reduction);
118 self.mapping_stale = false;
119 }
120
121 #[doc(hidden)]
123 pub fn clear_mapping(&mut self) {
124 self.mapping = None;
125 self.mapping_stale = false;
126 }
127
128 #[doc(hidden)]
130 pub fn retained_mapping(&self) -> Option<&TopologyMapping> {
131 self.mapping.as_ref()
132 }
133
134 pub fn set_switch_state(&mut self, switch_id: &str, open: bool) -> bool {
139 if let Some(sw) = self.switches.iter_mut().find(|s| s.id == switch_id)
140 && sw.open != open
141 {
142 sw.open = open;
143 self.mapping_stale = true;
146 return true;
147 }
148 false
149 }
150
151 pub fn is_current(&self) -> bool {
153 self.mapping.is_some() && !self.mapping_stale
154 }
155
156 pub fn status(&self) -> TopologyMappingState {
158 match (self.mapping.is_some(), self.mapping_stale) {
159 (false, _) => TopologyMappingState::Missing,
160 (true, false) => TopologyMappingState::Current,
161 (true, true) => TopologyMappingState::Stale,
162 }
163 }
164
165 pub fn current_mapping(&self) -> Option<&TopologyMapping> {
167 self.mapping.as_ref().filter(|_| self.is_current())
168 }
169
170 pub fn switch_state(&self, switch_id: &str) -> Option<bool> {
174 self.switches
175 .iter()
176 .find(|s| s.id == switch_id)
177 .map(|s| s.open)
178 }
179
180 pub fn switches_of_kind(&self, sw_type: SwitchType) -> Vec<&SwitchDevice> {
182 self.switches
183 .iter()
184 .filter(|s| s.switch_type == sw_type)
185 .collect()
186 }
187
188 pub fn bus_for_connectivity_node(&self, cn_id: &str) -> Option<u32> {
193 self.current_mapping()
194 .and_then(|m| m.connectivity_node_to_bus.get(cn_id).copied())
195 }
196
197 pub fn connectivity_nodes_for_bus(&self, bus_num: u32) -> Option<&Vec<String>> {
202 self.current_mapping()
203 .and_then(|m| m.bus_to_connectivity_nodes.get(&bus_num))
204 }
205}
206
207#[derive(Debug, Clone, Serialize, Deserialize)]
213pub struct Substation {
214 pub id: String,
216 pub name: String,
218 #[serde(default, skip_serializing_if = "Option::is_none")]
220 pub region: Option<String>,
221}
222
223#[derive(Debug, Clone, Serialize, Deserialize)]
225pub struct VoltageLevel {
226 pub id: String,
228 pub name: String,
230 pub substation_id: String,
232 pub base_kv: f64,
234}
235
236#[derive(Debug, Clone, Serialize, Deserialize)]
238pub struct Bay {
239 pub id: String,
241 pub name: String,
243 pub voltage_level_id: String,
245}
246
247#[derive(Debug, Clone, Serialize, Deserialize)]
249pub struct ConnectivityNode {
250 pub id: String,
252 pub name: String,
254 pub voltage_level_id: String,
256}
257
258#[derive(Debug, Clone, Serialize, Deserialize)]
260pub struct BusbarSection {
261 pub id: String,
263 pub name: String,
265 pub connectivity_node_id: String,
267 #[serde(default, skip_serializing_if = "Option::is_none")]
269 pub ip_max: Option<f64>,
270}
271
272#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
278pub enum SwitchType {
279 Breaker,
280 Disconnector,
281 LoadBreakSwitch,
282 Fuse,
283 GroundDisconnector,
284 Switch,
286}
287
288#[derive(Debug, Clone, Serialize, Deserialize)]
290pub struct SwitchDevice {
291 pub id: String,
293 pub name: String,
295 pub switch_type: SwitchType,
297 pub cn1_id: String,
299 pub cn2_id: String,
301 pub open: bool,
303 pub normal_open: bool,
305 #[serde(default)]
308 pub retained: bool,
309 #[serde(default, skip_serializing_if = "Option::is_none")]
311 pub rated_current: Option<f64>,
312}
313
314#[derive(Debug, Clone, Serialize, Deserialize)]
323pub struct TerminalConnection {
324 pub terminal_id: String,
326 pub equipment_id: String,
328 pub equipment_class: String,
330 pub sequence_number: u32,
332 pub connectivity_node_id: String,
334}
335
336#[derive(Debug, Clone, Default, Serialize, Deserialize)]
346pub struct TopologyMapping {
347 pub connectivity_node_to_bus: HashMap<String, u32>,
349
350 pub bus_to_connectivity_nodes: HashMap<u32, Vec<String>>,
352
353 #[serde(default, skip_serializing_if = "Vec::is_empty")]
355 pub consumed_switch_ids: Vec<String>,
356
357 #[serde(default, skip_serializing_if = "Vec::is_empty")]
359 pub isolated_connectivity_node_ids: Vec<String>,
360}
361
362#[cfg(test)]
367mod tests {
368 use super::*;
369
370 #[test]
371 fn substation_topology_default_is_empty() {
372 let sm = NodeBreakerTopology::default();
373 assert!(sm.substations.is_empty());
374 assert!(sm.switches.is_empty());
375 assert!(sm.retained_mapping().is_none());
376 assert_eq!(sm.status(), TopologyMappingState::Missing);
377 }
378
379 #[test]
380 fn set_switch_state_toggle() {
381 let mut sm = NodeBreakerTopology::new(
382 Vec::new(),
383 Vec::new(),
384 Vec::new(),
385 Vec::new(),
386 Vec::new(),
387 vec![SwitchDevice {
388 id: "BRK_1".into(),
389 name: "Breaker 1".into(),
390 switch_type: SwitchType::Breaker,
391 cn1_id: "CN_A".into(),
392 cn2_id: "CN_B".into(),
393 open: false,
394 normal_open: false,
395 retained: false,
396 rated_current: None,
397 }],
398 Vec::new(),
399 )
400 .with_mapping(TopologyMapping::default());
401
402 assert!(sm.set_switch_state("BRK_1", true));
404 assert_eq!(sm.switch_state("BRK_1"), Some(true));
405 assert!(sm.retained_mapping().is_some());
407 assert_eq!(sm.status(), TopologyMappingState::Stale);
408 assert_eq!(sm.bus_for_connectivity_node("CN_A"), None);
409
410 assert!(!sm.set_switch_state("BRK_1", true));
412
413 assert!(!sm.set_switch_state("BRK_UNKNOWN", false));
415 }
416
417 #[test]
418 fn switches_of_type_filter() {
419 let sm = NodeBreakerTopology::new(
420 Vec::new(),
421 Vec::new(),
422 Vec::new(),
423 Vec::new(),
424 Vec::new(),
425 vec![
426 SwitchDevice {
427 id: "BRK_1".into(),
428 name: "B1".into(),
429 switch_type: SwitchType::Breaker,
430 cn1_id: "A".into(),
431 cn2_id: "B".into(),
432 open: false,
433 normal_open: false,
434 retained: false,
435 rated_current: None,
436 },
437 SwitchDevice {
438 id: "DIS_1".into(),
439 name: "D1".into(),
440 switch_type: SwitchType::Disconnector,
441 cn1_id: "B".into(),
442 cn2_id: "C".into(),
443 open: false,
444 normal_open: false,
445 retained: false,
446 rated_current: None,
447 },
448 ],
449 Vec::new(),
450 );
451
452 assert_eq!(sm.switches_of_kind(SwitchType::Breaker).len(), 1);
453 assert_eq!(sm.switches_of_kind(SwitchType::Disconnector).len(), 1);
454 assert_eq!(sm.switches_of_kind(SwitchType::Fuse).len(), 0);
455 }
456
457 #[test]
458 fn serde_roundtrip() {
459 let sm = NodeBreakerTopology::new(
460 vec![Substation {
461 id: "SUB_1".into(),
462 name: "Station Alpha".into(),
463 region: Some("RGN_1".into()),
464 }],
465 vec![VoltageLevel {
466 id: "VL_220".into(),
467 name: "220 kV".into(),
468 substation_id: "SUB_1".into(),
469 base_kv: 220.0,
470 }],
471 Vec::new(),
472 vec![ConnectivityNode {
473 id: "CN_A".into(),
474 name: "Node A".into(),
475 voltage_level_id: "VL_220".into(),
476 }],
477 Vec::new(),
478 vec![SwitchDevice {
479 id: "BRK_1".into(),
480 name: "Breaker 1".into(),
481 switch_type: SwitchType::Breaker,
482 cn1_id: "CN_A".into(),
483 cn2_id: "CN_B".into(),
484 open: false,
485 normal_open: false,
486 retained: false,
487 rated_current: Some(2000.0),
488 }],
489 Vec::new(),
490 )
491 .with_mapping(TopologyMapping {
492 connectivity_node_to_bus: [("CN_A".into(), 1), ("CN_B".into(), 1)]
493 .into_iter()
494 .collect(),
495 bus_to_connectivity_nodes: [(1, vec!["CN_A".into(), "CN_B".into()])]
496 .into_iter()
497 .collect(),
498 consumed_switch_ids: vec!["BRK_1".into()],
499 isolated_connectivity_node_ids: vec![],
500 });
501
502 let json = serde_json::to_string(&sm).unwrap();
503 let deser: NodeBreakerTopology = serde_json::from_str(&json).unwrap();
504 assert_eq!(deser.substations.len(), 1);
505 assert_eq!(deser.switches.len(), 1);
506 assert_eq!(deser.switches[0].switch_type, SwitchType::Breaker);
507 assert_eq!(deser.bus_for_connectivity_node("CN_A"), Some(1));
508 assert_eq!(deser.connectivity_nodes_for_bus(1).unwrap().len(), 2);
509 assert!(deser.is_current());
510 assert_eq!(deser.status(), TopologyMappingState::Current);
511 }
512
513 #[test]
514 fn network_serde_without_substation_topology() {
515 let json = r#"{"name":"test","base_mva":100.0,"buses":[],"branches":[],"generators":[],"loads":[]}"#;
517 let net: crate::Network = serde_json::from_str(json).unwrap();
518 assert!(net.topology.is_none());
519 }
520}