powerio_prob/instance/
merge.rs1use std::collections::BTreeMap;
11
12use powerio_core::{Diagnostic, Error};
13use powerio_tx::{BalancedNetwork, BusId};
14
15use crate::diagnostics::codes;
16use crate::operating::row_identity;
17
18#[derive(Clone, Debug, Default, PartialEq)]
21#[non_exhaustive]
22pub struct ZeroImpedanceMerge {
23 pub merged_buses: BTreeMap<BusId, BusId>,
26 pub removed_branches: Vec<String>,
29}
30
31fn find(parent: &mut [usize], node: usize) -> usize {
43 let mut root = node;
44 while parent[root] != root {
45 root = parent[root];
46 }
47 let mut walk = node;
48 while parent[walk] != root {
49 let next = parent[walk];
50 parent[walk] = root;
51 walk = next;
52 }
53 root
54}
55
56#[allow(clippy::too_many_lines)] pub fn merge_zero_impedance_buses(
58 network: &BalancedNetwork,
59) -> Result<(BalancedNetwork, ZeroImpedanceMerge, Vec<Diagnostic>), Error> {
60 let mut diagnostics = Vec::new();
61
62 let index_of: BTreeMap<BusId, usize> = network
64 .buses()
65 .iter()
66 .enumerate()
67 .map(|(index, bus)| (bus.id, index))
68 .collect();
69 let mut parent: Vec<usize> = (0..network.buses().len()).collect();
70
71 let mut removed_rows = Vec::new();
72 for (row, branch) in network.branches().iter().enumerate() {
73 let zero = branch.r == 0.0 && branch.x == 0.0;
74 if !zero || !branch.in_service || branch.from == branch.to {
75 continue;
76 }
77 let (Some(&from), Some(&to)) = (index_of.get(&branch.from), index_of.get(&branch.to))
78 else {
79 return Err(Error::new(
80 &codes::BUILD_INSTANCE_SHAPE_MISMATCH,
81 format!(
82 "zero impedance branch row {row} names bus {} or {} the network does not declare",
83 branch.from, branch.to
84 ),
85 ));
86 };
87 let (from_root, to_root) = (find(&mut parent, from), find(&mut parent, to));
88 if from_root != to_root {
89 parent[from_root.max(to_root)] = from_root.min(to_root);
90 }
91 removed_rows.push(row);
92 let identity = row_identity(branch.uid.as_deref(), "branches", row);
93 diagnostics.push(Diagnostic::of(
94 &codes::CANONICALIZE_MERGE_ZERO_IMPEDANCE,
95 format!(
96 "zero impedance branch `{identity}` between buses {} and {} was merged; its flow is not a variable of any derived calculation",
97 branch.from, branch.to
98 ),
99 ));
100 }
101
102 if removed_rows.is_empty() {
103 return Ok((network.clone(), ZeroImpedanceMerge::default(), diagnostics));
104 }
105
106 let mut survivor_of_root: BTreeMap<usize, BusId> = BTreeMap::new();
110 for index in 0..network.buses().len() {
111 let root = find(&mut parent, index);
112 let id = network.buses()[index].id;
113 let entry = survivor_of_root.entry(root).or_insert(id);
114 if id < *entry {
115 *entry = id;
116 }
117 }
118 let mut merged_buses = BTreeMap::new();
119 for index in 0..network.buses().len() {
120 let root = find(&mut parent, index);
121 let id = network.buses()[index].id;
122 let survivor = survivor_of_root[&root];
123 if id != survivor {
124 merged_buses.insert(id, survivor);
125 }
126 }
127
128 let resolve = |bus: BusId| merged_buses.get(&bus).copied().unwrap_or(bus);
129
130 let mut merged = network.clone();
131 for (&gone, &kept) in &merged_buses {
134 let gone_bus = &network.buses()[index_of[&gone]];
135 let kept_bus = &network.buses()[index_of[&kept]];
136 if gone_bus.base_kv.to_bits() != kept_bus.base_kv.to_bits() {
140 diagnostics.push(Diagnostic::of(
141 &codes::CANONICALIZE_MERGE_ATTRIBUTE_CONFLICT,
142 format!(
143 "bus {gone} (base {} kV) merged into bus {kept} (base {} kV); the surviving base was kept",
144 gone_bus.base_kv, kept_bus.base_kv
145 ),
146 ));
147 }
148 if gone_bus.kind != kept_bus.kind && gone_bus.kind == powerio_tx::BusType::Ref {
149 let survivor = &mut merged.buses_mut()[index_of[&kept]];
151 survivor.kind = powerio_tx::BusType::Ref;
152 }
153 }
154
155 let removed: std::collections::BTreeSet<usize> = removed_rows.iter().copied().collect();
156 let removed_branches = removed_rows
157 .iter()
158 .map(|&row| row_identity(network.branches()[row].uid.as_deref(), "branches", row))
159 .collect();
160
161 merged
162 .buses_mut()
163 .retain(|bus| !merged_buses.contains_key(&bus.id));
164 let mut row = 0usize;
165 merged.branches_mut().retain(|_| {
166 let keep = !removed.contains(&row);
167 row += 1;
168 keep
169 });
170 for branch in merged.branches_mut() {
171 branch.from = resolve(branch.from);
172 branch.to = resolve(branch.to);
173 }
174 for load in merged.loads_mut() {
175 load.bus = resolve(load.bus);
176 }
177 for generator in merged.generators_mut() {
178 generator.bus = resolve(generator.bus);
179 }
180 for shunt in merged.shunts_mut() {
181 shunt.bus = resolve(shunt.bus);
182 }
183
184 Ok((
185 merged,
186 ZeroImpedanceMerge {
187 merged_buses,
188 removed_branches,
189 },
190 diagnostics,
191 ))
192}