use std::collections::HashSet;
use super::model::{BusIdx, NodeBreakerTopology, NodeIdx, SwitchIdx, SwitchKind};
use super::reduction::{UnionFind, merge_groups};
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub enum RetentionPolicy {
#[default]
MergeAll,
RetainAll,
RetainKinds(Vec<SwitchKind>),
RetainAdjacentToBusbar,
Explicit(HashSet<SwitchIdx>),
}
impl RetentionPolicy {
fn retains(&self, idx: SwitchIdx, topo: &NodeBreakerTopology) -> bool {
let switch = &topo.switches[idx.0];
if !switch.kind.is_operable() {
return false;
}
match self {
RetentionPolicy::MergeAll => false,
RetentionPolicy::RetainAll => true,
RetentionPolicy::RetainKinds(kinds) => kinds.contains(&switch.kind),
RetentionPolicy::RetainAdjacentToBusbar => {
switch.nodes.iter().any(|n| topo.busbars.contains(n))
}
RetentionPolicy::Explicit(set) => set.contains(&idx),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct RetainedSwitch {
pub switch: SwitchIdx,
pub buses: [BusIdx; 2],
pub open: bool,
}
#[derive(Clone, Debug)]
pub struct BusView {
bus_of: Vec<BusIdx>,
nodes_of: Vec<Vec<NodeIdx>>,
representative: Vec<NodeIdx>,
retained: Vec<RetainedSwitch>,
}
impl BusView {
pub fn n_buses(&self) -> usize {
self.nodes_of.len()
}
pub fn n_nodes(&self) -> usize {
self.bus_of.len()
}
pub fn bus_of(&self, node: NodeIdx) -> BusIdx {
self.bus_of[node.0]
}
pub fn nodes_of(&self, bus: BusIdx) -> &[NodeIdx] {
&self.nodes_of[bus.0]
}
pub fn representative(&self, bus: BusIdx) -> NodeIdx {
self.representative[bus.0]
}
pub fn retained(&self) -> &[RetainedSwitch] {
&self.retained
}
pub fn bus_of_slice(&self) -> &[BusIdx] {
&self.bus_of
}
pub fn is_degenerate(&self, retained: &RetainedSwitch) -> bool {
retained.buses[0] == retained.buses[1]
}
}
pub fn bus_view(topo: &NodeBreakerTopology, policy: &RetentionPolicy) -> BusView {
let mut uf = UnionFind::new(topo.n_nodes);
let mut retained_idx: Vec<SwitchIdx> = Vec::new();
for (i, switch) in topo.switches.iter().enumerate() {
let idx = SwitchIdx(i);
if policy.retains(idx, topo) {
retained_idx.push(idx);
continue;
}
if switch.conducting() {
uf.union(switch.nodes[0].0, switch.nodes[1].0);
}
}
let (remap, n_buses) = merge_groups(topo.n_nodes, &mut uf);
let bus_of: Vec<BusIdx> = remap.into_iter().map(BusIdx).collect();
let mut nodes_of: Vec<Vec<NodeIdx>> = vec![Vec::new(); n_buses];
let mut representative: Vec<NodeIdx> = vec![NodeIdx(0); n_buses];
for (node, bus) in bus_of.iter().enumerate() {
nodes_of[bus.0].push(NodeIdx(node));
representative[bus.0] = NodeIdx(uf.find(node));
}
let retained = retained_idx
.into_iter()
.map(|idx| {
let switch = &topo.switches[idx.0];
RetainedSwitch {
switch: idx,
buses: [bus_of[switch.nodes[0].0], bus_of[switch.nodes[1].0]],
open: switch.open,
}
})
.collect();
BusView {
bus_of,
nodes_of,
representative,
retained,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::topology::model::Switch;
fn sw(kind: SwitchKind, a: usize, b: usize, open: bool) -> Switch {
Switch {
kind,
nodes: [NodeIdx(a), NodeIdx(b)],
open,
in_service: true,
}
}
fn fixture() -> NodeBreakerTopology {
let mut topo = NodeBreakerTopology::new(4);
topo.add_switch(sw(SwitchKind::Breaker, 0, 1, false));
topo.add_switch(sw(SwitchKind::Disconnector, 1, 2, true));
topo
}
#[test]
fn merge_all_collapses_only_conducting_switches() {
let topo = fixture();
let view = bus_view(&topo, &RetentionPolicy::MergeAll);
assert_eq!(view.n_nodes(), 4);
assert_eq!(view.n_buses(), 3, "0 and 1 merge; 2 and 3 stay their own");
assert_eq!(view.bus_of(NodeIdx(0)), view.bus_of(NodeIdx(1)));
assert_ne!(view.bus_of(NodeIdx(1)), view.bus_of(NodeIdx(2)));
assert!(view.retained().is_empty());
assert_eq!(
view.nodes_of(view.bus_of(NodeIdx(0))),
&[NodeIdx(0), NodeIdx(1)]
);
}
#[test]
fn retain_all_keeps_every_operable_switch_and_merges_nothing() {
let topo = fixture();
let view = bus_view(&topo, &RetentionPolicy::RetainAll);
assert_eq!(view.n_buses(), 4, "nothing merged");
assert_eq!(view.retained().len(), 2);
let closed = view.retained().iter().find(|r| !r.open).unwrap();
assert_eq!(closed.buses, [BusIdx(0), BusIdx(1)]);
assert!(!view.is_degenerate(closed));
}
#[test]
fn retain_kinds_selects_by_class() {
let topo = fixture();
let view = bus_view(
&topo,
&RetentionPolicy::RetainKinds(vec![SwitchKind::Breaker]),
);
assert_eq!(view.n_buses(), 4);
assert_eq!(view.retained().len(), 1);
assert_eq!(view.retained()[0].switch, SwitchIdx(0));
}
#[test]
fn explicit_retains_exactly_what_it_names() {
let topo = fixture();
let policy = RetentionPolicy::Explicit(HashSet::from([SwitchIdx(1)]));
let view = bus_view(&topo, &policy);
assert_eq!(view.retained().len(), 1);
assert_eq!(view.retained()[0].switch, SwitchIdx(1));
assert_eq!(view.bus_of(NodeIdx(0)), view.bus_of(NodeIdx(1)));
}
#[test]
fn retain_adjacent_to_busbar_needs_busbars_and_says_so_when_absent() {
let mut topo = fixture();
assert!(
bus_view(&topo, &RetentionPolicy::RetainAdjacentToBusbar)
.retained()
.is_empty(),
"with no busbars declared, this policy retains nothing"
);
topo.busbars.push(NodeIdx(2));
let view = bus_view(&topo, &RetentionPolicy::RetainAdjacentToBusbar);
assert_eq!(view.retained().len(), 1, "only the switch touching node 2");
assert_eq!(view.retained()[0].switch, SwitchIdx(1));
}
#[test]
fn a_junction_is_never_retained() {
let mut topo = NodeBreakerTopology::new(2);
topo.add_switch(sw(SwitchKind::Junction, 0, 1, false));
topo.busbars.push(NodeIdx(0));
for policy in [
RetentionPolicy::RetainAll,
RetentionPolicy::RetainKinds(vec![SwitchKind::Junction]),
RetentionPolicy::RetainAdjacentToBusbar,
RetentionPolicy::Explicit(HashSet::from([SwitchIdx(0)])),
] {
let view = bus_view(&topo, &policy);
assert!(view.retained().is_empty(), "{policy:?} retained a junction");
assert_eq!(view.n_buses(), 1, "{policy:?} failed to merge a junction");
}
}
#[test]
fn an_out_of_service_switch_merges_nothing() {
let mut topo = NodeBreakerTopology::new(2);
topo.add_switch(Switch {
kind: SwitchKind::Breaker,
nodes: [NodeIdx(0), NodeIdx(1)],
open: false,
in_service: false,
});
assert_eq!(bus_view(&topo, &RetentionPolicy::MergeAll).n_buses(), 2);
}
#[test]
fn a_retained_switch_parallel_to_a_merged_path_is_degenerate() {
let mut topo = NodeBreakerTopology::new(2);
topo.add_switch(sw(SwitchKind::Breaker, 0, 1, false)); topo.add_switch(sw(SwitchKind::Junction, 0, 1, false));
let view = bus_view(&topo, &RetentionPolicy::RetainAll);
assert_eq!(view.n_buses(), 1);
assert_eq!(view.retained().len(), 1);
assert!(view.is_degenerate(&view.retained()[0]));
}
#[test]
fn the_representative_is_a_consistent_member_of_its_group() {
let mut topo = NodeBreakerTopology::new(5);
topo.add_switch(sw(SwitchKind::Breaker, 3, 4, false));
topo.add_switch(sw(SwitchKind::Breaker, 1, 2, false));
topo.add_switch(sw(SwitchKind::Breaker, 2, 3, false));
let view = bus_view(&topo, &RetentionPolicy::MergeAll);
for bus in 0..view.n_buses() {
let rep = view.representative(BusIdx(bus));
assert!(
view.nodes_of(BusIdx(bus)).contains(&rep),
"bus {bus}'s representative {rep:?} is not one of its nodes"
);
for &node in view.nodes_of(BusIdx(bus)) {
assert_eq!(view.representative(view.bus_of(node)), rep);
}
}
}
#[test]
fn bus_numbering_and_the_inverse_map_are_consistent() {
let mut topo = NodeBreakerTopology::new(5);
topo.add_switch(sw(SwitchKind::Breaker, 3, 4, false));
topo.add_switch(sw(SwitchKind::Breaker, 1, 2, false));
let view = bus_view(&topo, &RetentionPolicy::MergeAll);
assert_eq!(view.n_buses(), 3);
assert_eq!(view.bus_of(NodeIdx(0)), BusIdx(0));
assert_eq!(view.bus_of(NodeIdx(1)), BusIdx(1));
assert_eq!(view.bus_of(NodeIdx(3)), BusIdx(2));
for bus in 0..view.n_buses() {
for &node in view.nodes_of(BusIdx(bus)) {
assert_eq!(view.bus_of(node), BusIdx(bus));
}
}
let total: usize = (0..view.n_buses())
.map(|b| view.nodes_of(BusIdx(b)).len())
.sum();
assert_eq!(total, view.n_nodes(), "every node lands on exactly one bus");
}
}