1use std::collections::{BTreeMap, HashMap};
20
21use crate::diagnostics::{Diagnostic, codes};
22use crate::network::{BalancedNetwork, Bus, BusId, BusType};
23use crate::{Error, Result};
24
25pub const PSSE_DEFAULT_ZERO_IMPEDANCE_THRESHOLD: f64 = 0.0001;
29
30#[derive(Clone, Copy, Debug, PartialEq)]
35#[non_exhaustive]
36pub enum ZeroImpedanceRule {
37 Exact,
41 PsseThreshold(f64),
45 ImpedanceMagnitude(f64),
49}
50
51impl ZeroImpedanceRule {
52 #[allow(clippy::float_cmp)] fn matches(self, branch: &crate::network::Branch) -> bool {
55 if !branch.in_service {
56 return false;
57 }
58 match self {
59 ZeroImpedanceRule::Exact => {
60 branch.r == 0.0
61 && branch.x == 0.0
62 && branch.calc_effective_tap() == 1.0
63 && branch.shift == 0.0
64 }
65 ZeroImpedanceRule::PsseThreshold(threshold) => {
66 threshold > 0.0
67 && !branch.is_transformer()
68 && branch.r == 0.0
69 && branch.x.abs() <= threshold
70 }
71 ZeroImpedanceRule::ImpedanceMagnitude(threshold) => {
72 !branch.is_transformer() && branch.r.hypot(branch.x) <= threshold
73 }
74 }
75 }
76
77 fn threshold(self) -> Option<f64> {
78 match self {
79 ZeroImpedanceRule::Exact => None,
80 ZeroImpedanceRule::PsseThreshold(threshold)
81 | ZeroImpedanceRule::ImpedanceMagnitude(threshold) => Some(threshold),
82 }
83 }
84}
85
86impl std::fmt::Display for ZeroImpedanceRule {
87 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88 match self {
89 ZeroImpedanceRule::Exact => f.write_str("r == 0 and x == 0"),
90 ZeroImpedanceRule::PsseThreshold(threshold) => {
91 write!(f, "non-transformer, r == 0 and |x| <= {threshold}")
92 }
93 ZeroImpedanceRule::ImpedanceMagnitude(threshold) => {
94 write!(f, "non-transformer, |r + jx| <= {threshold}")
95 }
96 }
97 }
98}
99
100#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
103#[non_exhaustive]
104pub enum MergedCharging {
105 #[default]
110 FoldToShunt,
111 Drop,
114}
115
116#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
118#[non_exhaustive]
119pub enum MergeSurvivor {
120 #[default]
124 Strongest,
125 SmallestId,
128 FromBus,
132}
133
134#[derive(Clone, Copy, Debug, PartialEq)]
136#[non_exhaustive]
137pub struct BusMergeRule {
138 pub closed_switches: bool,
140 pub zero_impedance: Option<ZeroImpedanceRule>,
143 pub charging: MergedCharging,
145 pub survivor: MergeSurvivor,
147}
148
149impl BusMergeRule {
150 #[must_use]
153 pub const fn new(closed_switches: bool, zero_impedance: Option<ZeroImpedanceRule>) -> Self {
154 Self {
155 closed_switches,
156 zero_impedance,
157 charging: MergedCharging::FoldToShunt,
158 survivor: MergeSurvivor::Strongest,
159 }
160 }
161
162 #[must_use]
164 pub const fn with_charging(mut self, charging: MergedCharging) -> Self {
165 self.charging = charging;
166 self
167 }
168
169 #[must_use]
171 pub const fn with_survivor(mut self, survivor: MergeSurvivor) -> Self {
172 self.survivor = survivor;
173 self
174 }
175
176 #[must_use]
179 pub const fn exact() -> Self {
180 Self::new(true, Some(ZeroImpedanceRule::Exact))
181 }
182
183 #[must_use]
185 pub const fn closed_switches() -> Self {
186 Self::new(true, None)
187 }
188
189 pub fn psse(network: &BalancedNetwork) -> Result<Self> {
202 let threshold = network
203 .solver()
204 .as_ref()
205 .and_then(|solver| solver.zero_impedance_threshold)
206 .ok_or_else(|| Error::BusMergeRule {
207 message: "the case states no zero impedance threshold (PSS/E THRSHZ); give the \
208 threshold explicitly"
209 .to_owned(),
210 })?;
211 let rule = Self::new(true, Some(ZeroImpedanceRule::PsseThreshold(threshold)));
212 rule.check()?;
213 Ok(rule)
214 }
215
216 fn check(&self) -> Result<()> {
217 if let Some(threshold) = self.zero_impedance.and_then(ZeroImpedanceRule::threshold)
218 && !(threshold.is_finite() && threshold >= 0.0)
219 {
220 return Err(Error::BusMergeRule {
221 message: format!(
222 "the zero impedance threshold must be a finite nonnegative number, got {threshold}"
223 ),
224 });
225 }
226 Ok(())
227 }
228}
229
230impl std::fmt::Display for BusMergeRule {
231 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
232 match (self.closed_switches, self.zero_impedance) {
233 (true, Some(rule)) => write!(f, "closed switches and branches with {rule}"),
234 (false, Some(rule)) => write!(f, "branches with {rule}"),
235 (true, None) => f.write_str("closed switches"),
236 (false, None) => f.write_str("nothing"),
237 }
238 }
239}
240
241#[derive(Clone, Copy, Debug, PartialEq, Eq)]
243#[non_exhaustive]
244pub enum RemovalReason {
245 ZeroImpedance,
247 ClosedSwitch,
249 Shorted,
253}
254
255#[derive(Clone, Debug, PartialEq)]
257#[non_exhaustive]
258pub struct RemovedBranch {
259 pub row: usize,
261 pub identity: String,
263 pub from: BusId,
265 pub to: BusId,
266 pub survivor: BusId,
268 pub reason: RemovalReason,
269 pub in_service: bool,
270 pub r: f64,
271 pub x: f64,
272 pub b: f64,
275 pub rate_a: f64,
277 pub rate_b: f64,
278 pub rate_c: f64,
279}
280
281#[derive(Clone, Debug, PartialEq)]
283#[non_exhaustive]
284pub struct RemovedSwitch {
285 pub row: usize,
287 pub identity: String,
289 pub from: BusId,
290 pub to: BusId,
291 pub survivor: BusId,
292 pub reason: RemovalReason,
293 pub closed: bool,
294 pub thermal_rating: Option<f64>,
296}
297
298#[derive(Clone, Debug, PartialEq, Eq)]
300#[non_exhaustive]
301pub struct BusGroup {
302 pub survivor: BusId,
304 pub members: Vec<BusId>,
306}
307
308#[derive(Clone, Debug)]
310#[non_exhaustive]
311pub struct BusMerge {
312 pub network: BalancedNetwork,
314 pub source: BalancedNetwork,
316 pub rule: BusMergeRule,
318 pub merged_buses: BTreeMap<BusId, BusId>,
321 pub groups: Vec<BusGroup>,
323 pub removed_branches: Vec<RemovedBranch>,
325 pub removed_switches: Vec<RemovedSwitch>,
327 pub branch_rows: Vec<Option<usize>>,
330 pub switch_rows: Vec<Option<usize>>,
333 pub charging_shunts: Vec<usize>,
339 pub diagnostics: Vec<Diagnostic>,
341}
342
343impl BusMerge {
344 #[must_use]
347 pub fn survivor(&self, bus: BusId) -> BusId {
348 self.merged_buses.get(&bus).copied().unwrap_or(bus)
349 }
350
351 #[must_use]
353 pub fn is_identity(&self) -> bool {
354 self.merged_buses.is_empty()
355 && self.removed_branches.is_empty()
356 && self.removed_switches.is_empty()
357 }
358}
359
360#[derive(Clone, Copy, Debug)]
364#[non_exhaustive]
365pub struct MergedFlows<'a> {
366 pub branch_p_from: &'a [f64],
369 pub branch_p_to: &'a [f64],
372 pub generator_p: Option<&'a [f64]>,
375 pub transformer_3w_p: &'a [[f64; 3]],
378}
379
380impl<'a> MergedFlows<'a> {
381 #[must_use]
384 pub fn new(branch_p_from: &'a [f64], branch_p_to: &'a [f64]) -> Self {
385 Self {
386 branch_p_from,
387 branch_p_to,
388 generator_p: None,
389 transformer_3w_p: &[],
390 }
391 }
392
393 #[must_use]
395 pub fn with_generator_p(mut self, generator_p: &'a [f64]) -> Self {
396 self.generator_p = Some(generator_p);
397 self
398 }
399
400 #[must_use]
402 pub fn with_transformer_3w_p(mut self, transformer_3w_p: &'a [[f64; 3]]) -> Self {
403 self.transformer_3w_p = transformer_3w_p;
404 self
405 }
406}
407
408#[derive(Clone, Copy, Debug, PartialEq, Eq)]
410#[non_exhaustive]
411pub enum RemovedFlowMethod {
412 OutOfService,
414 Tree,
417 Reactance,
419 MinimumNorm,
422}
423
424#[derive(Clone, Copy, Debug, PartialEq)]
426#[non_exhaustive]
427pub struct RemovedFlow {
428 pub p_from: f64,
430 pub method: RemovedFlowMethod,
431}
432
433#[derive(Clone, Debug, PartialEq)]
435#[non_exhaustive]
436pub struct RemovedFlows {
437 pub branches: Vec<RemovedFlow>,
439 pub switches: Vec<RemovedFlow>,
441 pub diagnostics: Vec<Diagnostic>,
444}
445
446struct MergeSets {
455 parent: Vec<usize>,
456 windings: HashMap<usize, Vec<usize>>,
458 keep_first: bool,
461}
462
463enum Join {
464 Joined,
465 AlreadyJoined,
466 Refused(usize),
468}
469
470impl MergeSets {
471 fn new(n: usize, windings: HashMap<usize, Vec<usize>>, keep_first: bool) -> Self {
472 Self {
473 parent: (0..n).collect(),
474 windings,
475 keep_first,
476 }
477 }
478
479 fn root(&mut self, node: usize) -> usize {
480 let mut root = node;
481 while self.parent[root] != root {
482 root = self.parent[root];
483 }
484 let mut walk = node;
485 while self.parent[walk] != root {
486 let next = self.parent[walk];
487 self.parent[walk] = root;
488 walk = next;
489 }
490 root
491 }
492
493 fn join(&mut self, a: usize, b: usize) -> Join {
494 let (ra, rb) = (self.root(a), self.root(b));
495 if ra == rb {
496 return Join::AlreadyJoined;
497 }
498 if let (Some(wa), Some(wb)) = (self.windings.get(&ra), self.windings.get(&rb))
499 && let Some(&shared) = wa.iter().find(|t| wb.contains(t))
500 {
501 return Join::Refused(shared);
502 }
503 let (keep, gone) = if self.keep_first || ra < rb {
504 (ra, rb)
505 } else {
506 (rb, ra)
507 };
508 self.parent[gone] = keep;
509 if let Some(moved) = self.windings.remove(&gone) {
510 self.windings.entry(keep).or_default().extend(moved);
511 }
512 Join::Joined
513 }
514}
515
516fn remap_detailed_connectivity(
524 network: &mut BalancedNetwork,
525 resolve: &impl Fn(BusId) -> BusId,
526) -> Option<Diagnostic> {
527 let detailed = network.detailed_connectivity().as_deref()?;
528 let moved = |bus: BusId| resolve(bus) != bus;
529 let touches = detailed
530 .voltage_levels
531 .iter()
532 .any(|level| level.buses.iter().copied().any(moved))
533 || detailed
534 .bus_breaker_buses
535 .iter()
536 .any(|bus| bus.calculated_bus.is_some_and(moved))
537 || detailed
538 .connectivity_nodes
539 .iter()
540 .any(|node| node.calculated_bus.is_some_and(moved))
541 || detailed
542 .calculated_buses
543 .iter()
544 .any(|calculated| moved(calculated.calculated_bus));
545 if !touches {
546 return None;
547 }
548 let mut remapped = detailed.clone();
549 for level in &mut remapped.voltage_levels {
550 let mut seen = std::collections::HashSet::new();
551 level.buses = level
552 .buses
553 .iter()
554 .map(|&bus| resolve(bus))
555 .filter(|&bus| seen.insert(bus))
556 .collect();
557 }
558 for bus in &mut remapped.bus_breaker_buses {
559 bus.calculated_bus = bus.calculated_bus.map(resolve);
560 }
561 for node in &mut remapped.connectivity_nodes {
562 node.calculated_bus = node.calculated_bus.map(resolve);
563 }
564 let mut combined: Vec<crate::network::CalculatedBus> = Vec::new();
565 let mut position: HashMap<BusId, usize> = HashMap::new();
566 for mut calculated in std::mem::take(&mut remapped.calculated_buses) {
567 calculated.calculated_bus = resolve(calculated.calculated_bus);
568 if let Some(&kept) = position.get(&calculated.calculated_bus) {
569 combined[kept].nodes.extend(calculated.nodes);
570 } else {
571 position.insert(calculated.calculated_bus, combined.len());
572 combined.push(calculated);
573 }
574 }
575 remapped.calculated_buses = combined;
576 *network.detailed_connectivity_mut() = Some(std::sync::Arc::new(remapped));
577 if network.validate().is_ok() {
578 return None;
579 }
580
581 let detailed = std::sync::Arc::make_mut(
582 network
583 .detailed_connectivity_mut()
584 .as_mut()
585 .expect("the record was just written"),
586 );
587 for level in &mut detailed.voltage_levels {
588 level.buses.clear();
589 }
590 for bus in &mut detailed.bus_breaker_buses {
591 bus.calculated_bus = None;
592 }
593 for node in &mut detailed.connectivity_nodes {
594 node.calculated_bus = None;
595 }
596 detailed.calculated_buses.clear();
597 Some(Diagnostic::of(
598 &codes::CANONICALIZE_MERGE_DETAIL_DROPPED,
599 "the detailed connectivity's calculated bus assignments were dropped: the merge joined \
600 buses its hierarchy keeps apart, such as buses of two voltage levels",
601 ))
602}
603
604#[allow(clippy::float_cmp)] fn fold_admittance(
609 network: &mut BalancedNetwork,
610 source: &BalancedNetwork,
611 removed: &[RemovedBranch],
612 charging: MergedCharging,
613) -> Result<Vec<usize>> {
614 let scale = if source.is_normalized() {
615 1.0
616 } else {
617 source.base_mva()
618 };
619 let mut shunts = Vec::new();
620 for removed in removed.iter().filter(|removed| removed.in_service) {
621 let branch = &source.branches()[removed.row];
622 let tap = branch.calc_divisible_tap(removed.row)?;
623 let shift = if source.is_normalized() {
624 branch.shift
625 } else {
626 branch.shift.to_radians()
627 };
628 let (sin, cos) = shift.sin_cos();
629 let factor = (tap.recip() - cos).powi(2) + sin.powi(2);
632 let (mut g, mut b) = match branch.calc_series_admittance(removed.row)? {
633 Some((g, b)) => (g * factor, b * factor),
634 None if tap == 1.0 && shift == 0.0 => (0.0, 0.0),
635 None => return Err(Error::ZeroImpedance { row: removed.row }),
636 };
637 if charging == MergedCharging::FoldToShunt {
638 let terminal = branch.calc_terminal_charging();
639 let inverse_tap_squared = tap.recip().powi(2);
640 g += terminal.g_fr * inverse_tap_squared + terminal.g_to;
641 b += terminal.b_fr * inverse_tap_squared + terminal.b_to;
642 }
643 let (g, b) = (g * scale, b * scale);
644 if !g.is_finite() || !b.is_finite() {
645 return Err(Error::NonFiniteSusceptance { row: removed.row });
646 }
647 if g != 0.0 || b != 0.0 {
648 let mut shunt = crate::network::Shunt::new(removed.survivor, g, b);
649 shunt.uid = Some(format!("merged-charging:{}", removed.identity));
650 shunts.push(shunt);
651 }
652 }
653 let first = network.shunts().len();
654 let rows = (first..first + shunts.len()).collect();
655 if !shunts.is_empty() {
656 network.shunts_mut().extend(shunts);
657 }
658 Ok(rows)
659}
660
661const ATTRIBUTE_TOLERANCE: f64 = 1e-9;
664
665fn attribute_differs(a: f64, b: f64) -> bool {
668 let gap = (a - b).abs();
669 a.to_bits() != b.to_bits() && (gap.is_nan() || gap > ATTRIBUTE_TOLERANCE)
670}
671
672struct SurvivorAttributes {
674 kind: BusType,
675 vmin: f64,
676 vmax: f64,
677 va: f64,
678}
679
680fn merge_voltage_limits(
687 buses: &[Bus],
688 ids: &[BusId],
689 index_of: &HashMap<BusId, usize>,
690 survivor_index: usize,
691 diagnostics: &mut Vec<Diagnostic>,
692) -> (f64, f64) {
693 let survivor = &buses[survivor_index];
694 let members: Vec<&Bus> = ids.iter().map(|id| &buses[index_of[id]]).collect();
695 let tightest = |values: Vec<f64>, pick: fn(f64, f64) -> f64, own: f64| {
696 values
697 .into_iter()
698 .filter(|value| value.is_finite())
699 .reduce(pick)
700 .unwrap_or(own)
701 };
702 let vmin = tightest(
703 members.iter().map(|bus| bus.vmin).collect(),
704 f64::max,
705 survivor.vmin,
706 );
707 let vmax = tightest(
708 members.iter().map(|bus| bus.vmax).collect(),
709 f64::min,
710 survivor.vmax,
711 );
712 let differing: Vec<String> = members
713 .iter()
714 .filter(|bus| attribute_differs(bus.vmin, vmin) || attribute_differs(bus.vmax, vmax))
715 .map(|bus| bus.id.to_string())
716 .collect();
717 if differing.is_empty() {
718 return (vmin, vmax);
719 }
720 let bands = members
721 .iter()
722 .map(|bus| format!("bus {} [{}, {}]", bus.id, bus.vmin, bus.vmax))
723 .collect::<Vec<_>>()
724 .join(", ");
725 let id = survivor.id;
726 if vmin > vmax {
727 diagnostics.push(Diagnostic::of(
728 &codes::CANONICALIZE_MERGE_ATTRIBUTE_CONFLICT,
729 format!(
730 "buses merged into bus {id} have voltage limits that do not overlap ({bands} \
731 p.u.); bus {id} kept its own limits [{}, {}] p.u.",
732 survivor.vmin, survivor.vmax
733 ),
734 ));
735 return (survivor.vmin, survivor.vmax);
736 }
737 diagnostics.push(Diagnostic::of(
738 &codes::CANONICALIZE_MERGE_ATTRIBUTE_CONFLICT,
739 format!(
740 "buses merged into bus {id} have differing voltage limits ({bands} p.u.); bus {id} \
741 takes their intersection [{vmin}, {vmax}] p.u., which narrows the limits of \
742 bus(es) {}",
743 differing.join(", ")
744 ),
745 ));
746 (vmin, vmax)
747}
748
749fn merge_reference_angle(
754 buses: &[Bus],
755 references: &[BusId],
756 index_of: &HashMap<BusId, usize>,
757 survivor_index: usize,
758 diagnostics: &mut Vec<Diagnostic>,
759) -> f64 {
760 let survivor = &buses[survivor_index];
761 let Some(&first) = references.first() else {
762 return survivor.va;
763 };
764 let reference = if survivor.kind == BusType::Ref {
765 survivor
766 } else {
767 &buses[index_of[&first]]
768 };
769 let va = reference.va;
770 let mut angles: Vec<&Bus> = references.iter().map(|id| &buses[index_of[id]]).collect();
771 if survivor.kind != BusType::Ref {
772 angles.push(survivor);
773 angles.sort_by_key(|bus| bus.id);
774 }
775 if angles.iter().all(|bus| !attribute_differs(bus.va, va)) {
776 return va;
777 }
778 let listed = angles
779 .iter()
780 .map(|bus| format!("bus {} {} deg", bus.id, bus.va))
781 .collect::<Vec<_>>()
782 .join(", ");
783 let id = survivor.id;
784 diagnostics.push(Diagnostic::of(
785 &codes::CANONICALIZE_MERGE_ATTRIBUTE_CONFLICT,
786 format!(
787 "buses merged into bus {id} disagree on the reference angle ({listed}); bus {id} \
788 takes the angle {va} deg of reference bus {}",
789 reference.id
790 ),
791 ));
792 va
793}
794
795pub(crate) fn kind_priority(kind: BusType) -> u8 {
798 match kind {
799 BusType::Ref => 3,
800 BusType::Pv => 2,
801 BusType::Pq => 1,
802 BusType::Isolated => 0,
803 }
804}
805
806fn identity(uid: Option<&str>, table: &str, row: usize) -> String {
807 uid.map_or_else(|| format!("{table}:{row}"), str::to_owned)
808}
809
810fn counted(
812 info: &'static crate::diagnostics::DiagnosticInfo,
813 message: String,
814 counts: &[(&str, usize)],
815) -> Diagnostic {
816 let details = counts
817 .iter()
818 .map(|(key, count)| ((*key).to_owned(), serde_json::Value::from(*count)))
819 .collect();
820 Diagnostic::of(info, message)
821 .with_details(details)
822 .expect("a few short count entries are within the detail bounds")
823}
824
825impl BalancedNetwork {
826 #[allow(clippy::too_many_lines)] pub fn merge_buses(&self, rule: &BusMergeRule) -> Result<BusMerge> {
870 rule.check()?;
871 let buses = self.buses();
872 let index_of: HashMap<BusId, usize> = buses
873 .iter()
874 .enumerate()
875 .map(|(index, bus)| (bus.id, index))
876 .collect();
877 let lookup = |bus: BusId, row: usize| {
878 index_of.get(&bus).copied().ok_or(Error::UnknownBus {
879 bus_id: bus,
880 element_index: row,
881 })
882 };
883
884 let mut windings: HashMap<usize, Vec<usize>> = HashMap::new();
885 for (t, transformer) in self.transformers_3w().iter().enumerate() {
888 if !transformer.in_service {
889 continue;
890 }
891 let live = transformer.winding_in_service();
892 for (winding, _) in transformer
893 .windings
894 .iter()
895 .zip(live)
896 .filter(|(_, live)| *live)
897 {
898 if let Some(&index) = index_of.get(&winding.bus) {
899 let list = windings.entry(index).or_default();
900 if !list.contains(&t) {
901 list.push(t);
902 }
903 }
904 }
905 }
906 let mut sets = MergeSets::new(
907 buses.len(),
908 windings,
909 rule.survivor == MergeSurvivor::FromBus,
910 );
911 let mut diagnostics = Vec::new();
912 let mut refused = |what: String, from: BusId, to: BusId, t: usize| {
913 let transformer = &self.transformers_3w()[t];
914 let name = identity(transformer.uid.as_deref(), "transformers_3w", t);
915 diagnostics.push(Diagnostic::of(
916 &codes::CANONICALIZE_MERGE_WINDING_PAIR_KEPT,
917 format!(
918 "{what} between buses {from} and {to} was kept: merging it would join two \
919 windings of three winding transformer `{name}`"
920 ),
921 ));
922 };
923
924 if rule.closed_switches {
925 for (row, switch) in self.switches().iter().enumerate() {
926 if !switch.closed || switch.from == switch.to {
927 continue;
928 }
929 let (a, b) = (lookup(switch.from, row)?, lookup(switch.to, row)?);
930 if let Join::Refused(t) = sets.join(a, b) {
931 let name = identity(switch.uid.as_deref(), "switches", row);
932 refused(format!("closed switch `{name}`"), switch.from, switch.to, t);
933 }
934 }
935 }
936 let mut selected = vec![false; self.branches().len()];
937 if let Some(zero_impedance) = rule.zero_impedance {
938 for (row, branch) in self.branches().iter().enumerate() {
939 if branch.from == branch.to || !zero_impedance.matches(branch) {
940 continue;
941 }
942 let (a, b) = (lookup(branch.from, row)?, lookup(branch.to, row)?);
943 match sets.join(a, b) {
944 Join::Joined | Join::AlreadyJoined => selected[row] = true,
945 Join::Refused(t) => {
946 let name = identity(branch.uid.as_deref(), "branches", row);
947 refused(format!("branch `{name}`"), branch.from, branch.to, t);
948 }
949 }
950 }
951 }
952
953 let mut members_of: BTreeMap<usize, Vec<usize>> = BTreeMap::new();
955 for index in 0..buses.len() {
956 let root = sets.root(index);
957 if root != index {
958 members_of.entry(root).or_default().push(index);
959 }
960 }
961 for (&root, members) in &mut members_of {
963 members.push(root);
964 }
965 if members_of.is_empty() {
966 return Ok(self.identity_merge(*rule, diagnostics));
967 }
968
969 let mut hosts_generator = vec![false; buses.len()];
970 let mut regulated = vec![false; buses.len()];
971 for generator in self.generators().iter().filter(|g| g.in_service) {
972 if let Some(&index) = index_of.get(&generator.bus) {
973 hosts_generator[index] = true;
974 }
975 if let Some(index) = generator.regulated_bus.and_then(|b| index_of.get(&b)) {
976 regulated[*index] = true;
977 }
978 }
979 let rank = |index: usize| {
980 let bus = &buses[index];
981 (
982 bus.kind == BusType::Ref,
983 hosts_generator[index],
984 regulated[index],
985 std::cmp::Reverse(bus.id),
986 )
987 };
988
989 let mut merged_buses: HashMap<BusId, BusId> = HashMap::new();
990 let mut groups = Vec::with_capacity(members_of.len());
991 let mut survivor_attributes: HashMap<BusId, SurvivorAttributes> = HashMap::new();
992 for (&root, members) in &members_of {
993 let survivor_index = match rule.survivor {
994 MergeSurvivor::Strongest => {
995 members.iter().copied().max_by_key(|&index| rank(index))
996 }
997 MergeSurvivor::SmallestId => {
998 members.iter().copied().min_by_key(|&index| buses[index].id)
999 }
1000 MergeSurvivor::FromBus => None,
1001 }
1002 .unwrap_or(root);
1003 let survivor = buses[survivor_index].id;
1004 let kind = members
1005 .iter()
1006 .map(|&index| buses[index].kind)
1007 .max_by_key(|&kind| kind_priority(kind))
1008 .unwrap_or(buses[survivor_index].kind);
1009 let mut ids: Vec<BusId> = members.iter().map(|&index| buses[index].id).collect();
1010 ids.sort_unstable();
1011 for &id in &ids {
1012 if id != survivor {
1013 merged_buses.insert(id, survivor);
1014 }
1015 }
1016
1017 let base_kv = buses[survivor_index].base_kv;
1018 let mut other_kv: Vec<f64> = members
1019 .iter()
1020 .map(|&index| buses[index].base_kv)
1021 .filter(|kv| kv.to_bits() != base_kv.to_bits())
1022 .collect();
1023 if !other_kv.is_empty() {
1024 other_kv.sort_by(f64::total_cmp);
1025 other_kv.dedup_by(|a, b| a.to_bits() == b.to_bits());
1026 let other = other_kv
1027 .iter()
1028 .map(|kv| format!("{kv} kV"))
1029 .collect::<Vec<_>>()
1030 .join(", ");
1031 diagnostics.push(Diagnostic::of(
1032 &codes::CANONICALIZE_MERGE_ATTRIBUTE_CONFLICT,
1033 format!(
1034 "buses of base {other} merged into bus {survivor} (base {base_kv} kV); \
1035 the surviving base was kept"
1036 ),
1037 ));
1038 }
1039 let references: Vec<BusId> = ids
1040 .iter()
1041 .copied()
1042 .filter(|&id| buses[index_of[&id]].kind == BusType::Ref)
1043 .collect();
1044 if references.len() > 1 {
1045 let list = references
1046 .iter()
1047 .map(ToString::to_string)
1048 .collect::<Vec<_>>()
1049 .join(", ");
1050 diagnostics.push(Diagnostic::of(
1051 &codes::CANONICALIZE_MERGE_ATTRIBUTE_CONFLICT,
1052 format!(
1053 "reference buses {list} merged into one; bus {survivor} remains the \
1054 reference"
1055 ),
1056 ));
1057 }
1058 let (vmin, vmax) =
1059 merge_voltage_limits(buses, &ids, &index_of, survivor_index, &mut diagnostics);
1060 let va = merge_reference_angle(
1061 buses,
1062 &references,
1063 &index_of,
1064 survivor_index,
1065 &mut diagnostics,
1066 );
1067 survivor_attributes.insert(
1068 survivor,
1069 SurvivorAttributes {
1070 kind,
1071 vmin,
1072 vmax,
1073 va,
1074 },
1075 );
1076 groups.push(BusGroup {
1077 survivor,
1078 members: ids,
1079 });
1080 }
1081 groups.sort_by_key(|group| group.survivor);
1082 let resolve = |bus: BusId| merged_buses.get(&bus).copied().unwrap_or(bus);
1083
1084 let mut removed_branches = Vec::new();
1086 let mut branch_rows = Vec::with_capacity(self.branches().len());
1087 let mut next = 0usize;
1088 let (mut charged, mut rated) = (0usize, 0usize);
1089 for (row, branch) in self.branches().iter().enumerate() {
1090 let survivor = resolve(branch.from);
1091 if branch.from == branch.to || survivor != resolve(branch.to) {
1092 branch_rows.push(Some(next));
1093 next += 1;
1094 continue;
1095 }
1096 branch_rows.push(None);
1097 let reason = if selected[row] {
1098 RemovalReason::ZeroImpedance
1099 } else {
1100 RemovalReason::Shorted
1101 };
1102 let removed = RemovedBranch {
1103 row,
1104 identity: identity(branch.uid.as_deref(), "branches", row),
1105 from: branch.from,
1106 to: branch.to,
1107 survivor,
1108 reason,
1109 in_service: branch.in_service,
1110 r: branch.r,
1111 x: branch.x,
1112 b: branch.calc_total_charging_b(),
1113 rate_a: branch.rate_a,
1114 rate_b: branch.rate_b,
1115 rate_c: branch.rate_c,
1116 };
1117 if reason == RemovalReason::Shorted {
1118 let kind = if branch.is_transformer() {
1119 "transformer"
1120 } else {
1121 "branch"
1122 };
1123 diagnostics.push(Diagnostic::of(
1124 &codes::CANONICALIZE_MERGE_ELEMENT_SHORTED,
1125 format!(
1126 "{kind} `{}` between buses {} and {} was removed: the merge joined its \
1127 two buses into bus {survivor} through other elements",
1128 removed.identity, removed.from, removed.to
1129 ),
1130 ));
1131 } else {
1132 charged += usize::from(removed.b != 0.0);
1133 rated += usize::from(
1134 removed.rate_a > 0.0 || removed.rate_b > 0.0 || removed.rate_c > 0.0,
1135 );
1136 }
1137 removed_branches.push(removed);
1138 }
1139 let mut removed_switches = Vec::new();
1140 let mut switch_rows = Vec::with_capacity(self.switches().len());
1141 let mut next = 0usize;
1142 for (row, switch) in self.switches().iter().enumerate() {
1143 let survivor = resolve(switch.from);
1144 if switch.from == switch.to || survivor != resolve(switch.to) {
1145 switch_rows.push(Some(next));
1146 next += 1;
1147 continue;
1148 }
1149 switch_rows.push(None);
1150 let reason = if switch.closed && rule.closed_switches {
1151 RemovalReason::ClosedSwitch
1152 } else {
1153 RemovalReason::Shorted
1154 };
1155 let removed = RemovedSwitch {
1156 row,
1157 identity: identity(switch.uid.as_deref(), "switches", row),
1158 from: switch.from,
1159 to: switch.to,
1160 survivor,
1161 reason,
1162 closed: switch.closed,
1163 thermal_rating: switch.thermal_rating,
1164 };
1165 if reason == RemovalReason::Shorted {
1166 let state = if switch.closed { "closed" } else { "open" };
1167 diagnostics.push(Diagnostic::of(
1168 &codes::CANONICALIZE_MERGE_ELEMENT_SHORTED,
1169 format!(
1170 "{state} switch `{}` between buses {} and {} was removed: the merge \
1171 joined its two buses into bus {survivor} through other elements",
1172 removed.identity, removed.from, removed.to
1173 ),
1174 ));
1175 }
1176 removed_switches.push(removed);
1177 }
1178 for (row, line) in self.hvdc().iter().enumerate() {
1179 if line.from != line.to && resolve(line.from) == resolve(line.to) {
1180 let name = identity(line.uid.as_deref(), "hvdc", row);
1181 diagnostics.push(Diagnostic::of(
1182 &codes::CANONICALIZE_MERGE_ELEMENT_SHORTED,
1183 format!(
1184 "HVDC line `{name}` between buses {} and {} now has both ends on bus {}; \
1185 it was kept",
1186 line.from,
1187 line.to,
1188 resolve(line.from)
1189 ),
1190 ));
1191 }
1192 }
1193
1194 let zero_impedance = removed_branches
1195 .iter()
1196 .filter(|removed| removed.reason == RemovalReason::ZeroImpedance)
1197 .count();
1198 if let Some(branch_rule) = rule.zero_impedance
1199 && zero_impedance > 0
1200 {
1201 let charging = match rule.charging {
1202 MergedCharging::FoldToShunt => "folded onto the survivors as fixed shunts",
1203 MergedCharging::Drop => "dropped",
1204 };
1205 diagnostics.push(counted(
1206 &codes::CANONICALIZE_MERGE_ZERO_IMPEDANCE,
1207 format!(
1208 "{zero_impedance} in-service branch(es) with {branch_rule} were merged \
1209 ({rated} with a thermal rating, {charged} with line charging, {charging}); \
1210 their flows are no longer variables of the merged network, and \
1211 BusMerge::calc_removed_flows recovers them from a solution"
1212 ),
1213 &[
1214 ("count", zero_impedance),
1215 ("rated", rated),
1216 ("charged", charged),
1217 ],
1218 ));
1219 }
1220 let closed = removed_switches
1221 .iter()
1222 .filter(|removed| removed.reason == RemovalReason::ClosedSwitch)
1223 .count();
1224 if closed > 0 {
1225 diagnostics.push(counted(
1226 &codes::CANONICALIZE_MERGE_CLOSED_SWITCH,
1227 format!(
1228 "{closed} closed switch(es) were merged; their flows are no longer variables \
1229 of the merged network, and BusMerge::calc_removed_flows recovers them from a \
1230 solution"
1231 ),
1232 &[("count", closed)],
1233 ));
1234 }
1235
1236 let mut network = self.clone();
1237 {
1238 let mut row = 0usize;
1239 network.branches_mut().retain(|_| {
1240 let keep = branch_rows[row].is_some();
1241 row += 1;
1242 keep
1243 });
1244 if !removed_switches.is_empty() {
1245 let mut row = 0usize;
1246 network.switches_mut().retain(|_| {
1247 let keep = switch_rows[row].is_some();
1248 row += 1;
1249 keep
1250 });
1251 }
1252 }
1253 network.remap_buses(resolve);
1254 let charging_shunts =
1255 fold_admittance(&mut network, self, &removed_branches, rule.charging)?;
1256 network.buses_mut().retain_mut(|bus| {
1257 if merged_buses.contains_key(&bus.id) {
1258 return false;
1259 }
1260 if let Some(attributes) = survivor_attributes.get(&bus.id) {
1261 bus.kind = attributes.kind;
1262 bus.vmin = attributes.vmin;
1263 bus.vmax = attributes.vmax;
1264 bus.va = attributes.va;
1265 }
1266 true
1267 });
1268 if let Some(diagnostic) = remap_detailed_connectivity(&mut network, &resolve) {
1269 diagnostics.push(diagnostic);
1270 }
1271 debug_assert!(
1272 network.validate().is_ok(),
1273 "merge_buses produced a dangling reference"
1274 );
1275
1276 Ok(BusMerge {
1277 network,
1278 source: self.clone(),
1279 rule: *rule,
1280 merged_buses: merged_buses.into_iter().collect(),
1281 groups,
1282 removed_branches,
1283 removed_switches,
1284 branch_rows,
1285 switch_rows,
1286 charging_shunts,
1287 diagnostics,
1288 })
1289 }
1290
1291 fn identity_merge(&self, rule: BusMergeRule, diagnostics: Vec<Diagnostic>) -> BusMerge {
1292 BusMerge {
1293 network: self.clone(),
1294 source: self.clone(),
1295 rule,
1296 merged_buses: BTreeMap::new(),
1297 groups: Vec::new(),
1298 removed_branches: Vec::new(),
1299 removed_switches: Vec::new(),
1300 branch_rows: (0..self.branches().len()).map(Some).collect(),
1301 switch_rows: (0..self.switches().len()).map(Some).collect(),
1302 charging_shunts: Vec::new(),
1303 diagnostics,
1304 }
1305 }
1306}
1307
1308const MAX_DENSE_LOOP_BUSES: usize = 2000;
1311
1312#[derive(Clone, Copy)]
1314struct GroupEdge {
1315 from: usize,
1316 to: usize,
1317 x: f64,
1319 slot: Slot,
1320}
1321
1322#[derive(Clone, Copy)]
1323enum Slot {
1324 Branch(usize),
1325 Switch(usize),
1326}
1327
1328impl BusMerge {
1329 #[allow(clippy::too_many_lines)] pub fn calc_removed_flows(&self, flows: &MergedFlows<'_>) -> Result<RemovedFlows> {
1363 let shape = |what: &'static str, expected: usize, got: usize| {
1364 if expected == got {
1365 Ok(())
1366 } else {
1367 Err(Error::MergedFlowShape {
1368 what,
1369 expected,
1370 got,
1371 })
1372 }
1373 };
1374 let merged_branches = self.network.branches().len();
1375 shape("branch_p_from", merged_branches, flows.branch_p_from.len())?;
1376 shape("branch_p_to", merged_branches, flows.branch_p_to.len())?;
1377 let source = &self.source;
1378 if let Some(generator_p) = flows.generator_p {
1379 shape("generator_p", source.generators().len(), generator_p.len())?;
1380 }
1381 if !flows.transformer_3w_p.is_empty() {
1382 shape(
1383 "transformer_3w_p",
1384 source.transformers_3w().len(),
1385 flows.transformer_3w_p.len(),
1386 )?;
1387 }
1388
1389 let mut place: HashMap<BusId, (usize, usize)> = HashMap::new();
1390 for (group, members) in self.groups.iter().enumerate() {
1391 for (local, &bus) in members.members.iter().enumerate() {
1392 place.insert(bus, (group, local));
1393 }
1394 }
1395 let mut injection: Vec<Vec<f64>> = self
1396 .groups
1397 .iter()
1398 .map(|group| vec![0.0; group.members.len()])
1399 .collect();
1400 let mut device = vec![0.0; self.groups.len()];
1404 let mut leaving = vec![0.0; self.groups.len()];
1405 let mut scale = vec![0.0f64; self.groups.len()];
1406 let mut inject = |bus: BusId, value: f64, from_device: bool| {
1407 if let Some(&(group, local)) = place.get(&bus) {
1408 injection[group][local] += value;
1409 if from_device {
1410 device[group] += value;
1411 } else {
1412 leaving[group] -= value;
1413 }
1414 scale[group] += value.abs();
1415 }
1416 };
1417
1418 for (row, generator) in source.generators().iter().enumerate() {
1419 if generator.in_service {
1420 let p = flows.generator_p.map_or(generator.pg, |p| p[row]);
1421 inject(generator.bus, p, true);
1422 }
1423 }
1424 for load in source.loads().iter().filter(|load| load.in_service) {
1425 inject(load.bus, -load.p, true);
1426 }
1427 for shunt in source.shunts().iter().filter(|shunt| shunt.in_service) {
1428 inject(shunt.bus, -shunt.g, true);
1429 }
1430 for storage in source.storage().iter().filter(|storage| storage.in_service) {
1431 inject(storage.bus, -storage.ps, true);
1432 }
1433 for line in source.hvdc().iter().filter(|line| line.in_service) {
1434 inject(line.from, -line.pf, true);
1435 inject(line.to, line.pt, true);
1436 }
1437 for (row, merged_row) in self.branch_rows.iter().enumerate() {
1438 if let Some(merged_row) = *merged_row {
1439 let branch = &source.branches()[row];
1440 inject(branch.from, -flows.branch_p_from[merged_row], false);
1441 inject(branch.to, -flows.branch_p_to[merged_row], false);
1442 }
1443 }
1444 for (row, transformer) in source.transformers_3w().iter().enumerate() {
1445 if !transformer.in_service {
1446 continue;
1447 }
1448 for (winding, terminal) in transformer.windings.iter().enumerate() {
1449 if !place.contains_key(&terminal.bus) {
1450 continue;
1451 }
1452 let Some(p) = flows.transformer_3w_p.get(row) else {
1453 return Err(Error::MergedFlowShape {
1454 what: "transformer_3w_p",
1455 expected: source.transformers_3w().len(),
1456 got: flows.transformer_3w_p.len(),
1457 });
1458 };
1459 inject(terminal.bus, -p[winding], false);
1460 }
1461 }
1462
1463 let references: std::collections::HashSet<BusId> = source
1464 .buses()
1465 .iter()
1466 .filter(|bus| bus.kind == BusType::Ref)
1467 .map(|bus| bus.id)
1468 .collect();
1469 let mut diagnostics = Vec::new();
1470 for (index, group) in self.groups.iter().enumerate() {
1471 let residual = leaving[index] - device[index];
1472 let local = group
1473 .members
1474 .iter()
1475 .position(|&bus| bus == group.survivor)
1476 .unwrap_or_default();
1477 injection[index][local] += residual;
1478 let has_reference = group.members.iter().any(|bus| references.contains(bus));
1479 let tolerance = 1e-6 * scale[index].max(1.0);
1480 if residual.abs() > tolerance && (flows.generator_p.is_some() || !has_reference) {
1481 diagnostics.push(Diagnostic::of(
1482 &codes::CANONICALIZE_MERGE_FLOW_RESIDUAL,
1483 format!(
1484 "the group merged into bus {} receives {residual} more from the merged \
1485 solution than its stated devices inject; the difference was placed at \
1486 bus {}, so its removed element flows carry it",
1487 group.survivor, group.survivor
1488 ),
1489 ));
1490 }
1491 }
1492
1493 let not_carried = RemovedFlow {
1494 p_from: 0.0,
1495 method: RemovedFlowMethod::OutOfService,
1496 };
1497 let mut branches = vec![not_carried; self.removed_branches.len()];
1498 let mut switches = vec![not_carried; self.removed_switches.len()];
1499 let mut edges: Vec<Vec<GroupEdge>> = vec![Vec::new(); self.groups.len()];
1500 let local = |bus: BusId| place[&bus];
1501 for (index, removed) in self.removed_branches.iter().enumerate() {
1502 if removed.in_service {
1503 let ((group, from), (_, to)) = (local(removed.from), local(removed.to));
1504 edges[group].push(GroupEdge {
1505 from,
1506 to,
1507 x: removed.x,
1508 slot: Slot::Branch(index),
1509 });
1510 }
1511 }
1512 for (index, removed) in self.removed_switches.iter().enumerate() {
1513 if removed.closed {
1514 let ((group, from), (_, to)) = (local(removed.from), local(removed.to));
1515 edges[group].push(GroupEdge {
1516 from,
1517 to,
1518 x: 0.0,
1519 slot: Slot::Switch(index),
1520 });
1521 }
1522 }
1523
1524 for (index, group) in self.groups.iter().enumerate() {
1525 if edges[index].is_empty() {
1526 continue;
1527 }
1528 let solved = split_group(group.members.len(), &edges[index], &injection[index]);
1529 for (edge, flow) in edges[index].iter().zip(&solved.flows) {
1530 match edge.slot {
1531 Slot::Branch(row) => branches[row] = *flow,
1532 Slot::Switch(row) => switches[row] = *flow,
1533 }
1534 }
1535 if solved.minimum_norm > 0 {
1536 diagnostics.push(Diagnostic::of(
1537 &codes::CANONICALIZE_MERGE_FLOW_UNDETERMINED,
1538 format!(
1539 "{} removed element(s) in the group merged into bus {} lie on loops of \
1540 zero reactance elements; no network quantity fixes their split, so the \
1541 minimum norm split is reported",
1542 solved.minimum_norm, group.survivor
1543 ),
1544 ));
1545 }
1546 if solved.too_large > 0 {
1547 diagnostics.push(Diagnostic::of(
1548 &codes::CANONICALIZE_MERGE_FLOW_UNDETERMINED,
1549 format!(
1550 "{} removed element(s) in the group merged into bus {} lie on a loop of \
1551 more than {MAX_DENSE_LOOP_BUSES} buses, which is not split; their flows \
1552 are NaN",
1553 solved.too_large, group.survivor
1554 ),
1555 ));
1556 }
1557 }
1558
1559 Ok(RemovedFlows {
1560 branches,
1561 switches,
1562 diagnostics,
1563 })
1564 }
1565}
1566
1567struct SplitGroup {
1568 flows: Vec<RemovedFlow>,
1569 minimum_norm: usize,
1570 too_large: usize,
1571}
1572
1573fn split_group(members: usize, edges: &[GroupEdge], injection: &[f64]) -> SplitGroup {
1580 let mut flows = vec![
1581 RemovedFlow {
1582 p_from: 0.0,
1583 method: RemovedFlowMethod::Reactance,
1584 };
1585 edges.len()
1586 ];
1587 let mut too_large = 0;
1588
1589 let mut sets = MergeSets::new(members, HashMap::new(), false);
1591 for edge in edges.iter().filter(|edge| edge.x == 0.0) {
1592 let _ = sets.join(edge.from, edge.to);
1593 }
1594 let mut collapsed_of = vec![usize::MAX; members];
1595 let mut collapsed = 0usize;
1596 for node in 0..members {
1597 let root = sets.root(node);
1598 if collapsed_of[root] == usize::MAX {
1599 collapsed_of[root] = collapsed;
1600 collapsed += 1;
1601 }
1602 collapsed_of[node] = collapsed_of[root];
1603 }
1604 let mut collapsed_injection = vec![0.0; collapsed];
1605 for (node, value) in injection.iter().enumerate() {
1606 collapsed_injection[collapsed_of[node]] += value;
1607 }
1608
1609 let reactive: Vec<usize> = (0..edges.len())
1612 .filter(|&e| edges[e].x != 0.0 && collapsed_of[edges[e].from] != collapsed_of[edges[e].to])
1613 .collect();
1614 let mut remaining = injection.to_vec();
1615 if !reactive.is_empty() {
1616 let links: Vec<(usize, usize, f64)> = reactive
1617 .iter()
1618 .map(|&e| {
1619 (
1620 collapsed_of[edges[e].from],
1621 collapsed_of[edges[e].to],
1622 edges[e].x,
1623 )
1624 })
1625 .collect();
1626 let solved = split_network(collapsed, &links, &collapsed_injection);
1627 for (&e, (flow, kind)) in reactive.iter().zip(solved) {
1628 flows[e] = RemovedFlow {
1629 p_from: flow,
1630 method: match kind {
1631 Split::Tree => RemovedFlowMethod::Tree,
1632 Split::Loop => RemovedFlowMethod::Reactance,
1633 Split::TooLarge => {
1634 too_large += 1;
1635 RemovedFlowMethod::Reactance
1636 }
1637 },
1638 };
1639 remaining[edges[e].from] -= flow;
1640 remaining[edges[e].to] += flow;
1641 }
1642 }
1643
1644 let zero: Vec<usize> = (0..edges.len()).filter(|&e| edges[e].x == 0.0).collect();
1646 let mut minimum_norm = 0;
1647 if !zero.is_empty() {
1648 let links: Vec<(usize, usize, f64)> = zero
1649 .iter()
1650 .map(|&e| (edges[e].from, edges[e].to, 1.0))
1651 .collect();
1652 let solved = split_network(members, &links, &remaining);
1653 for (&e, (flow, kind)) in zero.iter().zip(solved) {
1654 flows[e] = RemovedFlow {
1655 p_from: flow,
1656 method: match kind {
1657 Split::Tree => RemovedFlowMethod::Tree,
1658 Split::Loop => {
1659 minimum_norm += 1;
1660 RemovedFlowMethod::MinimumNorm
1661 }
1662 Split::TooLarge => {
1663 too_large += 1;
1664 RemovedFlowMethod::MinimumNorm
1665 }
1666 },
1667 };
1668 }
1669 }
1670 SplitGroup {
1671 flows,
1672 minimum_norm,
1673 too_large,
1674 }
1675}
1676
1677#[derive(Clone, Copy)]
1678enum Split {
1679 Tree,
1680 Loop,
1681 TooLarge,
1682}
1683
1684fn split_network(
1688 nodes: usize,
1689 links: &[(usize, usize, f64)],
1690 injection: &[f64],
1691) -> Vec<(f64, Split)> {
1692 let mut result = vec![(0.0, Split::Tree); links.len()];
1693 let mut net = injection.to_vec();
1694 let mut incident = vec![Vec::new(); nodes];
1695 for (index, &(from, to, _)) in links.iter().enumerate() {
1696 incident[from].push(index);
1697 incident[to].push(index);
1698 }
1699 let mut degree: Vec<usize> = incident.iter().map(Vec::len).collect();
1700 let mut alive = vec![true; links.len()];
1701 let mut leaves: Vec<usize> = (0..nodes).filter(|&node| degree[node] == 1).collect();
1702 while let Some(leaf) = leaves.pop() {
1703 let Some(&index) = incident[leaf].iter().find(|&&index| alive[index]) else {
1704 continue;
1705 };
1706 alive[index] = false;
1707 let (from, to, _) = links[index];
1708 let other = if from == leaf { to } else { from };
1709 let out = net[leaf];
1710 result[index] = (if from == leaf { out } else { -out }, Split::Tree);
1711 net[other] += out;
1712 net[leaf] = 0.0;
1713 degree[leaf] = 0;
1714 degree[other] -= 1;
1715 if degree[other] == 1 {
1716 leaves.push(other);
1717 }
1718 }
1719
1720 let mut sets = MergeSets::new(nodes, HashMap::new(), false);
1722 for (index, &(from, to, _)) in links.iter().enumerate() {
1723 if alive[index] {
1724 let _ = sets.join(from, to);
1725 }
1726 }
1727 let mut cores: BTreeMap<usize, (Vec<usize>, Vec<usize>)> = BTreeMap::new();
1728 for node in (0..nodes).filter(|&node| degree[node] > 0) {
1729 cores.entry(sets.root(node)).or_default().0.push(node);
1730 }
1731 for (index, &(from, _, _)) in links.iter().enumerate() {
1732 if alive[index] {
1733 cores.entry(sets.root(from)).or_default().1.push(index);
1734 }
1735 }
1736 for (members, link_indices) in cores.values() {
1737 if members.len() > MAX_DENSE_LOOP_BUSES {
1738 for &index in link_indices {
1739 result[index] = (f64::NAN, Split::TooLarge);
1740 }
1741 continue;
1742 }
1743 let solved = solve_loop(members, links, link_indices, &net).unwrap_or_else(|| {
1744 let unit: Vec<(usize, usize, f64)> =
1747 links.iter().map(|&(from, to, _)| (from, to, 1.0)).collect();
1748 solve_loop(members, &unit, link_indices, &net)
1749 .expect("a unit weight Laplacian of a connected graph is nonsingular")
1750 });
1751 for (index, flow) in solved {
1752 result[index] = (flow, Split::Loop);
1753 }
1754 }
1755 result
1756}
1757
1758fn solve_loop(
1761 members: &[usize],
1762 links: &[(usize, usize, f64)],
1763 link_indices: &[usize],
1764 injection: &[f64],
1765) -> Option<Vec<(usize, f64)>> {
1766 let position: HashMap<usize, usize> = members
1767 .iter()
1768 .enumerate()
1769 .map(|(position, &node)| (node, position))
1770 .collect();
1771 let size = members.len() - 1;
1772 let mut matrix = vec![0.0; size * size];
1773 let mut rhs: Vec<f64> = members[1..].iter().map(|&node| injection[node]).collect();
1774 for &index in link_indices {
1775 let (from, to, x) = links[index];
1776 let conductance = 1.0 / x;
1777 let (i, j) = (position[&from], position[&to]);
1778 for (a, b, value) in [
1779 (i, i, conductance),
1780 (j, j, conductance),
1781 (i, j, -conductance),
1782 (j, i, -conductance),
1783 ] {
1784 if a > 0 && b > 0 {
1785 matrix[(a - 1) * size + (b - 1)] += value;
1786 }
1787 }
1788 }
1789 let angle = solve_dense(&mut matrix, &mut rhs, size)?;
1790 let theta = |node: usize| match position[&node] {
1791 0 => 0.0,
1792 p => angle[p - 1],
1793 };
1794 Some(
1795 link_indices
1796 .iter()
1797 .map(|&index| {
1798 let (from, to, x) = links[index];
1799 (index, (theta(from) - theta(to)) / x)
1800 })
1801 .collect(),
1802 )
1803}
1804
1805fn solve_dense(matrix: &mut [f64], rhs: &mut [f64], size: usize) -> Option<Vec<f64>> {
1807 let largest = matrix.iter().fold(0.0f64, |m, v| m.max(v.abs()));
1808 let floor = largest * 1e-13;
1809 for column in 0..size {
1810 let pivot = (column..size).max_by(|&a, &b| {
1811 matrix[a * size + column]
1812 .abs()
1813 .total_cmp(&matrix[b * size + column].abs())
1814 })?;
1815 if matrix[pivot * size + column].abs() <= floor
1816 || !matrix[pivot * size + column].is_finite()
1817 {
1818 return None;
1819 }
1820 if pivot != column {
1821 for k in 0..size {
1822 matrix.swap(pivot * size + k, column * size + k);
1823 }
1824 rhs.swap(pivot, column);
1825 }
1826 let diagonal = matrix[column * size + column];
1827 for row in column + 1..size {
1828 let factor = matrix[row * size + column] / diagonal;
1829 if factor == 0.0 {
1830 continue;
1831 }
1832 for k in column..size {
1833 matrix[row * size + k] -= factor * matrix[column * size + k];
1834 }
1835 rhs[row] -= factor * rhs[column];
1836 }
1837 }
1838 let mut solution = vec![0.0; size];
1839 for row in (0..size).rev() {
1840 let mut value = rhs[row];
1841 for k in row + 1..size {
1842 value -= matrix[row * size + k] * solution[k];
1843 }
1844 solution[row] = value / matrix[row * size + row];
1845 }
1846 Some(solution)
1847}
1848
1849#[cfg(test)]
1850#[allow(clippy::float_cmp)] mod tests {
1852 use super::*;
1853 use crate::network::{
1854 Area, Branch, Bus, Generator, Hvdc, Impedance, Load, Shunt, SolverParams, Switch,
1855 SwitchedShuntControl, SwitchedShuntMode, Transformer3W, TransformerControl,
1856 TransformerControlMode, Winding,
1857 };
1858
1859 const PSSE: BusMergeRule =
1860 BusMergeRule::new(true, Some(ZeroImpedanceRule::PsseThreshold(1e-4)));
1861
1862 fn net(ids: &[usize], branches: Vec<Branch>) -> BalancedNetwork {
1863 let buses = ids
1864 .iter()
1865 .map(|&id| Bus::new(BusId(id), BusType::Pq, 230.0))
1866 .collect();
1867 BalancedNetwork::in_memory("merge", 100.0, buses, branches)
1868 }
1869
1870 fn line(from: usize, to: usize, r: f64, x: f64) -> Branch {
1871 Branch::new(BusId(from), BusId(to), r, x)
1872 }
1873
1874 fn jumper(from: usize, to: usize) -> Branch {
1875 line(from, to, 0.0, 5e-5)
1876 }
1877
1878 fn transformer_3w(buses: [usize; 3]) -> Transformer3W {
1879 Transformer3W::new(
1880 buses.map(|bus| Winding::new(BusId(bus))),
1881 [Impedance::new(0.0, 0.1, 100.0); 3],
1882 )
1883 }
1884
1885 fn codes_of(diagnostics: &[Diagnostic]) -> Vec<&str> {
1886 diagnostics.iter().map(Diagnostic::code).collect()
1887 }
1888
1889 #[test]
1890 fn a_ring_of_closed_switches_merges_and_an_open_switch_stays() {
1891 let mut network = net(&[1, 2, 3, 4, 5, 6], vec![line(5, 6, 0.01, 0.1)]);
1892 network.switches_mut().extend([
1893 Switch::new(BusId(1), BusId(2), true),
1894 Switch::new(BusId(2), BusId(3), true),
1895 Switch::new(BusId(3), BusId(4), true),
1896 Switch::new(BusId(4), BusId(1), true),
1897 Switch::new(BusId(4), BusId(5), false),
1898 ]);
1899 let merge = network
1900 .merge_buses(&BusMergeRule::closed_switches())
1901 .unwrap();
1902
1903 assert_eq!(
1904 merge.groups,
1905 vec![BusGroup {
1906 survivor: BusId(1),
1907 members: vec![BusId(1), BusId(2), BusId(3), BusId(4)],
1908 }]
1909 );
1910 assert_eq!(merge.survivor(BusId(4)), BusId(1));
1911 assert_eq!(merge.survivor(BusId(5)), BusId(5));
1912 assert_eq!(merge.removed_switches.len(), 4);
1913 assert!(
1914 merge
1915 .removed_switches
1916 .iter()
1917 .all(|removed| removed.reason == RemovalReason::ClosedSwitch)
1918 );
1919 assert_eq!(
1920 merge.switch_rows,
1921 vec![None, None, None, None, Some(0)],
1922 "the open switch keeps its identity at merged row 0"
1923 );
1924 let open = &merge.network.switches()[0];
1925 assert_eq!(
1926 (open.from, open.to, open.closed),
1927 (BusId(1), BusId(5), false)
1928 );
1929 assert_eq!(merge.network.buses().len(), 3);
1930 assert!(codes_of(&merge.diagnostics).contains(&"CANONICALIZE.MERGE.CLOSED_SWITCH"));
1931 merge.network.validate().unwrap();
1932 }
1933
1934 #[test]
1935 fn the_psse_threshold_merges_a_chain_of_jumpers() {
1936 let network = net(
1937 &[1, 2, 3, 4, 5],
1938 vec![
1939 jumper(1, 2),
1940 jumper(2, 3),
1941 jumper(3, 4),
1942 line(4, 5, 0.01, 0.1),
1943 ],
1944 );
1945 let merge = network.merge_buses(&PSSE).unwrap();
1946 assert_eq!(merge.groups.len(), 1);
1947 assert_eq!(merge.groups[0].members.len(), 4);
1948 assert_eq!(merge.removed_branches.len(), 3);
1949 assert_eq!(merge.branch_rows, vec![None, None, None, Some(0)]);
1950 let kept = &merge.network.branches()[0];
1951 assert_eq!((kept.from, kept.to), (BusId(1), BusId(5)));
1952 let summary = merge
1953 .diagnostics
1954 .iter()
1955 .find(|d| d.code() == "CANONICALIZE.MERGE.ZERO_IMPEDANCE")
1956 .unwrap();
1957 assert_eq!(summary.details()["count"], 3);
1958 }
1959
1960 #[test]
1961 fn a_merged_jumper_s_charging_becomes_a_shunt_at_the_survivor() {
1962 let mut charged = jumper(2, 3);
1963 charged.b = 0.02;
1964 let network = net(&[1, 2, 3], vec![line(1, 2, 0.01, 0.1), charged]);
1965 let merge = network.merge_buses(&PSSE).unwrap();
1966 assert_eq!(merge.charging_shunts, [0]);
1967 let shunt = &merge.network.shunts()[0];
1968 assert_eq!(shunt.bus, BusId(2));
1969 assert!(
1970 (shunt.b - 2.0).abs() < 1e-12,
1971 "0.02 pu on 100 MVA is 2 MVAr"
1972 );
1973 assert_eq!(shunt.g, 0.0);
1974 assert!(
1975 shunt
1976 .uid
1977 .as_deref()
1978 .is_some_and(|uid| uid.starts_with("merged-charging:"))
1979 );
1980 merge.network.validate().unwrap();
1981
1982 let dropped = network
1983 .merge_buses(&PSSE.with_charging(MergedCharging::Drop))
1984 .unwrap();
1985 assert_eq!(dropped.charging_shunts, Vec::<usize>::new());
1986 assert_eq!(dropped.network.shunts().as_slice(), []);
1987 let summary = dropped
1988 .diagnostics
1989 .iter()
1990 .find(|d| d.code() == "CANONICALIZE.MERGE.ZERO_IMPEDANCE")
1991 .unwrap();
1992 assert!(summary.message().contains("dropped"));
1993 assert_eq!(summary.details()["charged"], 1);
1994 }
1995
1996 #[test]
1997 fn the_psse_threshold_skips_resistive_jumpers_and_transformers() {
1998 let mut transformer = jumper(3, 4);
1999 transformer.tap = 1.0;
2000 let network = net(
2001 &[1, 2, 3, 4],
2002 vec![line(1, 2, 1e-5, 5e-5), line(2, 3, 0.01, 0.1), transformer],
2003 );
2004 let merge = network.merge_buses(&PSSE).unwrap();
2005 assert!(
2006 merge.is_identity(),
2007 "R > 0 and transformers are not PSS/E jumpers"
2008 );
2009
2010 let wider = BusMergeRule::new(false, Some(ZeroImpedanceRule::ImpedanceMagnitude(1e-3)));
2011 let merge = network.merge_buses(&wider).unwrap();
2012 assert_eq!(merge.merged_buses, BTreeMap::from([(BusId(2), BusId(1))]));
2013 assert_eq!(merge.removed_branches.len(), 1, "the transformer stays");
2014 }
2015
2016 #[test]
2017 fn a_zero_threshold_disables_the_psse_rule() {
2018 let network = net(&[1, 2], vec![line(1, 2, 0.0, 0.0)]);
2019 let off = BusMergeRule::new(false, Some(ZeroImpedanceRule::PsseThreshold(0.0)));
2020 assert!(network.merge_buses(&off).unwrap().is_identity());
2021 let exact = network.merge_buses(&BusMergeRule::exact()).unwrap();
2022 assert_eq!(exact.removed_branches.len(), 1);
2023 }
2024
2025 #[test]
2026 fn the_exact_rule_keeps_an_off_nominal_ideal_transformer() {
2027 let mut ideal = line(1, 2, 0.0, 0.0);
2028 ideal.tap = 1.05;
2029 let mut nominal = line(2, 3, 0.0, 0.0);
2030 nominal.tap = 1.0;
2031 let network = net(&[1, 2, 3], vec![ideal, nominal]);
2032 let merge = network.merge_buses(&BusMergeRule::exact()).unwrap();
2033 assert_eq!(merge.merged_buses, BTreeMap::from([(BusId(3), BusId(2))]));
2034 }
2035
2036 #[test]
2037 fn the_survivor_is_the_reference_then_a_generator_then_a_regulated_bus() {
2038 let chain = |ids: &[usize]| {
2041 let mut network = net(ids, ids.windows(2).map(|w| jumper(w[0], w[1])).collect());
2042 network.generators_mut().push(Generator::new(BusId(5)));
2043 network.generators_mut()[0].regulated_bus = Some(BusId(2));
2044 network
2045 };
2046 let mut network = chain(&[1, 2, 3, 4, 5]);
2047 network.generators_mut().push(Generator::new(BusId(3)));
2048 network.buses_mut()[3].kind = BusType::Ref;
2049 let merge = network.merge_buses(&PSSE).unwrap();
2050 assert_eq!(merge.groups[0].survivor, BusId(4));
2051 let survivor = &merge.network.buses()[0];
2052 assert_eq!((survivor.id, survivor.kind), (BusId(4), BusType::Ref));
2053
2054 let mut network = chain(&[1, 2, 3, 6]);
2055 network.generators_mut()[0].bus = BusId(6);
2056 network.generators_mut().push(Generator::new(BusId(3)));
2057 network.generators_mut()[1].in_service = true;
2058 let merge = network.merge_buses(&PSSE).unwrap();
2059 assert_eq!(
2060 merge.groups[0].survivor,
2061 BusId(3),
2062 "generator over lower id"
2063 );
2064
2065 let mut network = chain(&[1, 2, 3, 5]);
2066 network.generators_mut()[0].in_service = true;
2067 network.generators_mut()[0].bus = BusId(7);
2068 network
2069 .buses_mut()
2070 .push(Bus::new(BusId(7), BusType::Pv, 230.0));
2071 let merge = network.merge_buses(&PSSE).unwrap();
2072 assert_eq!(
2073 merge.groups[0].survivor,
2074 BusId(2),
2075 "regulated over lower id"
2076 );
2077
2078 let network = net(&[3, 1, 2], vec![jumper(3, 1), jumper(1, 2)]);
2079 let merge = network.merge_buses(&PSSE).unwrap();
2080 assert_eq!(merge.groups[0].survivor, BusId(1), "lowest id");
2081 }
2082
2083 #[test]
2084 fn the_survivor_rule_can_keep_the_smallest_id_or_the_from_bus() {
2085 let mut network = net(&[1, 3, 4], vec![jumper(3, 1), jumper(4, 3)]);
2088 network.buses_mut()[2].kind = BusType::Ref;
2089 network.generators_mut().push(Generator::new(BusId(3)));
2090 let survivor = |rule: MergeSurvivor| {
2091 let merge = network.merge_buses(&PSSE.with_survivor(rule)).unwrap();
2092 let bus = &merge.network.buses()[0];
2093 assert_eq!(merge.network.buses().len(), 1);
2094 assert_eq!(bus.kind, BusType::Ref, "the strongest kind survives");
2095 assert_eq!(merge.network.generators()[0].bus, bus.id);
2096 merge.groups[0].survivor
2097 };
2098 assert_eq!(survivor(MergeSurvivor::Strongest), BusId(4));
2099 assert_eq!(survivor(MergeSurvivor::SmallestId), BusId(1));
2100 assert_eq!(survivor(MergeSurvivor::FromBus), BusId(4));
2102
2103 let network = net(&[1, 2, 3], vec![jumper(2, 1), jumper(2, 3)]);
2104 let merge = network
2105 .merge_buses(&PSSE.with_survivor(MergeSurvivor::FromBus))
2106 .unwrap();
2107 assert_eq!(merge.groups[0].survivor, BusId(2));
2108 assert_eq!(merge.survivor(BusId(1)), BusId(2));
2109 assert_eq!(merge.survivor(BusId(3)), BusId(2));
2110 }
2111
2112 #[test]
2113 fn every_reference_follows_the_survivor() {
2114 let mut network = net(
2115 &[1, 2, 3, 4, 5, 6],
2116 vec![jumper(1, 2), line(2, 3, 0.01, 0.1), line(3, 4, 0.01, 0.1)],
2117 );
2118 network.buses_mut()[0].kind = BusType::Ref;
2119 let mut regulating = Generator::new(BusId(3));
2120 regulating.regulated_bus = Some(BusId(2));
2121 network.generators_mut().push(regulating);
2122 network.loads_mut().push(Load::new(BusId(2), 10.0, 1.0));
2123 let mut shunt = Shunt::new(BusId(3), 0.0, 10.0);
2124 let mut control =
2125 SwitchedShuntControl::new(SwitchedShuntMode::Discrete, 1.05, 0.95, vec![]);
2126 control.control_bus = Some(BusId(2));
2127 shunt.control = Some(control);
2128 network.shunts_mut().push(shunt);
2129 let mut tap_control = TransformerControl::new(TransformerControlMode::Voltage);
2130 tap_control.controlled_bus = Some(BusId(2));
2131 network.branches_mut()[2].tap = 1.0;
2132 network.branches_mut()[2].control = Some(tap_control);
2133 network.hvdc_mut().push(Hvdc::new(BusId(2), BusId(5)));
2134 network
2135 .transformers_3w_mut()
2136 .push(transformer_3w([2, 5, 6]));
2137 let mut area = Area::new(1);
2138 area.slack_bus = Some(BusId(2));
2139 network.areas_mut().push(area);
2140 network
2141 .static_var_compensators_mut()
2142 .push(crate::network::StaticVarCompensator::new(
2143 BusId(2),
2144 -0.01,
2145 0.01,
2146 ));
2147 network
2148 .storage_mut()
2149 .push(crate::network::Storage::new(BusId(2)));
2150 network
2151 .switches_mut()
2152 .push(Switch::new(BusId(2), BusId(4), false));
2153
2154 let merge = network.merge_buses(&PSSE).unwrap();
2155 let merged = &merge.network;
2156 assert_eq!(merge.merged_buses, BTreeMap::from([(BusId(2), BusId(1))]));
2157 assert_eq!(merged.generators()[0].regulated_bus, Some(BusId(1)));
2158 assert_eq!(merged.loads()[0].bus, BusId(1));
2159 let control = merged.shunts()[0].control.as_ref().unwrap();
2160 assert_eq!(control.control_bus, Some(BusId(1)));
2161 let tap = merged.branches()[1].control.as_ref().unwrap();
2162 assert_eq!(tap.controlled_bus, Some(BusId(1)));
2163 assert_eq!(merged.branches()[0].from, BusId(1));
2164 assert_eq!(merged.hvdc()[0].from, BusId(1));
2165 assert_eq!(merged.transformers_3w()[0].windings[0].bus, BusId(1));
2166 assert_eq!(merged.areas()[0].slack_bus, Some(BusId(1)));
2167 assert_eq!(merged.static_var_compensators()[0].bus, BusId(1));
2168 assert_eq!(merged.storage()[0].bus, BusId(1));
2169 assert_eq!(merged.switches()[0].from, BusId(1));
2170 merged.validate().unwrap();
2171 assert_eq!(network.loads()[0].bus, BusId(2));
2173 }
2174
2175 #[test]
2176 fn a_jumper_across_two_windings_of_one_transformer_is_kept() {
2177 let mut network = net(
2178 &[1, 2, 3, 4],
2179 vec![jumper(1, 2), jumper(3, 4), jumper(2, 4)],
2180 );
2181 network
2184 .transformers_3w_mut()
2185 .push(transformer_3w([1, 3, 4]));
2186 let merge = network.merge_buses(&PSSE).unwrap();
2187 assert_eq!(
2188 merge.groups.len(),
2189 1,
2190 "only 1-2 merges; 3-4 joins two windings"
2191 );
2192 assert_eq!(merge.network.branches().len(), 2);
2193 let kept: Vec<&Diagnostic> = merge
2194 .diagnostics
2195 .iter()
2196 .filter(|d| d.code() == "CANONICALIZE.MERGE.WINDING_PAIR_KEPT")
2197 .collect();
2198 assert_eq!(kept.len(), 2, "{:#?}", merge.diagnostics);
2199 merge.network.validate().unwrap();
2200 }
2201
2202 #[test]
2203 fn merging_the_result_again_joins_nothing() {
2204 let mut network = net(
2205 &[1, 2, 3, 4, 5],
2206 vec![
2207 jumper(1, 2),
2208 jumper(2, 3),
2209 line(3, 4, 0.01, 0.1),
2210 jumper(4, 5),
2211 ],
2212 );
2213 network
2214 .switches_mut()
2215 .push(Switch::new(BusId(5), BusId(1), true));
2216 network
2217 .transformers_3w_mut()
2218 .push(transformer_3w([1, 3, 5]));
2219 let once = network.merge_buses(&PSSE).unwrap();
2220 assert!(!once.is_identity());
2221 let twice = once.network.merge_buses(&PSSE).unwrap();
2222 assert!(twice.is_identity());
2223 assert_eq!(twice.network.buses(), once.network.buses());
2224 }
2225
2226 #[test]
2227 fn the_psse_rule_reads_the_stated_threshold() {
2228 let mut network = net(&[1, 2], vec![line(1, 2, 0.0, 3e-4)]);
2229 let error = BusMergeRule::psse(&network).unwrap_err();
2230 assert_eq!(error.code().code, "CANONICALIZE.MERGE.INVALID_RULE");
2231
2232 let mut solver = SolverParams::new();
2233 solver.zero_impedance_threshold = Some(5e-4);
2234 *network.solver_mut() = Some(solver);
2235 let rule = BusMergeRule::psse(&network).unwrap();
2236 assert_eq!(
2237 rule,
2238 BusMergeRule::new(true, Some(ZeroImpedanceRule::PsseThreshold(5e-4)))
2239 );
2240 assert_eq!(
2241 network.merge_buses(&rule).unwrap().removed_branches.len(),
2242 1
2243 );
2244
2245 for threshold in [-1e-4, f64::NAN, f64::INFINITY] {
2246 let rule = BusMergeRule::new(false, Some(ZeroImpedanceRule::PsseThreshold(threshold)));
2247 assert!(network.merge_buses(&rule).is_err(), "{threshold}");
2248 }
2249 }
2250
2251 #[test]
2252 fn differing_base_voltages_and_two_references_are_reported() {
2253 let mut network = net(&[1, 2, 3], vec![jumper(1, 2), jumper(2, 3)]);
2254 network.buses_mut()[1].base_kv = 34.5;
2255 network.buses_mut()[0].kind = BusType::Ref;
2256 network.buses_mut()[2].kind = BusType::Ref;
2257 let merge = network.merge_buses(&PSSE).unwrap();
2258 let conflicts: Vec<&str> = merge
2259 .diagnostics
2260 .iter()
2261 .filter(|d| d.code() == "CANONICALIZE.MERGE.ATTRIBUTE_CONFLICT")
2262 .map(Diagnostic::message)
2263 .collect();
2264 assert_eq!(conflicts.len(), 2, "{conflicts:?}");
2265 assert!(conflicts[0].contains("34.5 kV"));
2266 assert!(conflicts[1].contains("reference buses 1, 3"));
2267 assert_eq!(merge.network.buses()[0].kind, BusType::Ref);
2268 }
2269
2270 fn conflicts(merge: &BusMerge) -> Vec<&str> {
2271 merge
2272 .diagnostics
2273 .iter()
2274 .filter(|d| d.code() == "CANONICALIZE.MERGE.ATTRIBUTE_CONFLICT")
2275 .map(Diagnostic::message)
2276 .collect()
2277 }
2278
2279 #[test]
2280 fn a_merge_intersects_differing_voltage_limits_and_reports_them() {
2281 let mut network = net(
2282 &[1, 2, 3, 4],
2283 vec![jumper(1, 2), jumper(2, 3), line(3, 4, 0.0, 0.1)],
2284 );
2285 network.buses_mut()[1].vmin = 0.95;
2286 network.buses_mut()[2].vmax = 1.05;
2287 network.buses_mut()[2].vmin = 0.93;
2288 let merge = network.merge_buses(&PSSE).unwrap();
2289 let survivor = &merge.network.buses()[0];
2290 assert_eq!(survivor.id, BusId(1));
2291 assert_eq!((survivor.vmin, survivor.vmax), (0.95, 1.05));
2292 let conflicts = conflicts(&merge);
2293 assert_eq!(conflicts.len(), 1, "{conflicts:?}");
2294 assert!(
2295 conflicts[0].contains("intersection [0.95, 1.05]"),
2296 "{}",
2297 conflicts[0]
2298 );
2299 assert!(conflicts[0].contains("bus(es) 1, 2, 3"), "{}", conflicts[0]);
2300 assert_eq!(merge.network.buses()[1].vmin, 0.9);
2302 }
2303
2304 #[test]
2305 fn equal_voltage_limits_and_angles_merge_without_a_conflict() {
2306 let mut network = net(&[1, 2, 3], vec![jumper(1, 2), jumper(2, 3)]);
2307 for bus in network.buses_mut() {
2308 (bus.vmin, bus.vmax, bus.va) = (0.94, 1.06, -3.0);
2309 }
2310 network.buses_mut()[2].vmax = 1.06 + 1e-12;
2312 network.buses_mut()[1].kind = BusType::Ref;
2313 let merge = network
2314 .merge_buses(&PSSE.with_survivor(MergeSurvivor::SmallestId))
2315 .unwrap();
2316 assert_eq!(conflicts(&merge), Vec::<&str>::new());
2317 let survivor = &merge.network.buses()[0];
2318 assert_eq!((survivor.vmin, survivor.vmax), (0.94, 1.06));
2319 assert_eq!(survivor.kind, BusType::Ref);
2320 }
2321
2322 #[test]
2323 fn disjoint_voltage_limits_keep_the_survivor_s_own() {
2324 let mut network = net(&[1, 2], vec![jumper(1, 2)]);
2325 (network.buses_mut()[1].vmin, network.buses_mut()[1].vmax) = (1.15, 1.2);
2326 let merge = network.merge_buses(&PSSE).unwrap();
2327 let survivor = &merge.network.buses()[0];
2328 assert_eq!((survivor.vmin, survivor.vmax), (0.9, 1.1));
2329 let conflicts = conflicts(&merge);
2330 assert_eq!(conflicts.len(), 1, "{conflicts:?}");
2331 assert!(conflicts[0].contains("do not overlap"), "{}", conflicts[0]);
2332 }
2333
2334 #[test]
2335 fn a_survivor_taking_over_a_reference_takes_its_angle() {
2336 let mut network = net(&[1, 2, 3], vec![jumper(1, 2), jumper(2, 3)]);
2337 network.buses_mut()[0].va = 4.0;
2338 network.buses_mut()[1].va = -7.5;
2339 network.buses_mut()[1].kind = BusType::Ref;
2340 let merge = network
2341 .merge_buses(&PSSE.with_survivor(MergeSurvivor::SmallestId))
2342 .unwrap();
2343 let survivor = &merge.network.buses()[0];
2344 assert_eq!(survivor.id, BusId(1));
2345 assert_eq!(survivor.kind, BusType::Ref);
2346 assert_eq!(survivor.va, -7.5);
2347 let conflicts = conflicts(&merge);
2348 assert_eq!(conflicts.len(), 1, "{conflicts:?}");
2349 assert!(
2350 conflicts[0].contains("bus 1 4 deg, bus 2 -7.5 deg"),
2351 "{}",
2352 conflicts[0]
2353 );
2354 assert!(conflicts[0].contains("reference bus 2"), "{}", conflicts[0]);
2355 }
2356
2357 #[test]
2358 fn a_line_in_parallel_with_a_jumper_is_shorted_and_an_hvdc_line_stays() {
2359 let mut network = net(&[1, 2, 3], vec![jumper(1, 2), line(1, 2, 0.01, 0.1)]);
2360 network.hvdc_mut().push(Hvdc::new(BusId(1), BusId(2)));
2361 network
2362 .switches_mut()
2363 .push(Switch::new(BusId(2), BusId(1), false));
2364 let merge = network.merge_buses(&PSSE).unwrap();
2365 let reasons: Vec<RemovalReason> = merge
2366 .removed_branches
2367 .iter()
2368 .map(|removed| removed.reason)
2369 .collect();
2370 assert_eq!(
2371 reasons,
2372 [RemovalReason::ZeroImpedance, RemovalReason::Shorted]
2373 );
2374 assert_eq!(merge.removed_switches[0].reason, RemovalReason::Shorted);
2375 assert_eq!(merge.network.hvdc().len(), 1);
2376 let shorted = codes_of(&merge.diagnostics)
2377 .into_iter()
2378 .filter(|code| *code == "CANONICALIZE.MERGE.ELEMENT_SHORTED")
2379 .count();
2380 assert_eq!(shorted, 3);
2381 }
2382
2383 #[test]
2384 fn detailed_connectivity_follows_the_survivor() {
2385 use crate::network::{DetailedConnectivity, TopologyKind, VoltageLevel};
2386 let mut network = net(&[1, 2, 3], vec![jumper(1, 2), line(2, 3, 0.01, 0.1)]);
2387 let level = |name: &str, buses: Vec<BusId>| VoltageLevel {
2388 component: powerio_core::ComponentId::new("voltage_level", name).unwrap(),
2389 substation: None,
2390 nominal_kv: 230.0,
2391 low_voltage_limit_kv: None,
2392 high_voltage_limit_kv: None,
2393 topology_kind: TopologyKind::BusBreaker,
2394 buses,
2395 };
2396 let detailed = DetailedConnectivity {
2397 voltage_levels: vec![
2398 level("A", vec![BusId(1), BusId(2)]),
2399 level("B", vec![BusId(3)]),
2400 ],
2401 ..DetailedConnectivity::default()
2402 };
2403 *network.detailed_connectivity_mut() = Some(std::sync::Arc::new(detailed));
2404 network.validate().unwrap();
2405
2406 let merge = network.merge_buses(&PSSE).unwrap();
2407 let levels = &merge
2408 .network
2409 .detailed_connectivity()
2410 .as_deref()
2411 .unwrap()
2412 .voltage_levels;
2413 assert_eq!(levels[0].buses, vec![BusId(1)]);
2414 assert!(!codes_of(&merge.diagnostics).contains(&"CANONICALIZE.MERGE.DETAIL_DROPPED"));
2415
2416 network.branches_mut()[1] = jumper(2, 3);
2419 let merge = network.merge_buses(&PSSE).unwrap();
2420 let detailed = merge.network.detailed_connectivity().as_deref().unwrap();
2421 assert!(
2422 detailed
2423 .voltage_levels
2424 .iter()
2425 .all(|level| level.buses.is_empty())
2426 );
2427 assert!(codes_of(&merge.diagnostics).contains(&"CANONICALIZE.MERGE.DETAIL_DROPPED"));
2428 merge.network.validate().unwrap();
2429 }
2430
2431 fn fed_group(extra: Vec<Branch>) -> BalancedNetwork {
2434 let mut branches = vec![line(1, 2, 0.01, 0.1)];
2435 branches.extend(extra);
2436 let mut network = net(&[1, 2, 3, 4], branches);
2437 network.buses_mut()[0].kind = BusType::Ref;
2438 network.generators_mut().push(Generator::new(BusId(1)));
2439 network.loads_mut().push(Load::new(BusId(3), 30.0, 0.0));
2440 network.loads_mut().push(Load::new(BusId(4), 50.0, 0.0));
2441 network
2442 }
2443
2444 #[test]
2445 fn removed_flows_follow_kirchhoff_on_a_tree() {
2446 let network = fed_group(vec![jumper(2, 3), jumper(3, 4)]);
2447 let merge = network.merge_buses(&PSSE).unwrap();
2448 let flows = merge
2450 .calc_removed_flows(&MergedFlows::new(&[80.0], &[-80.0]))
2451 .unwrap();
2452 let p: Vec<f64> = flows.branches.iter().map(|flow| flow.p_from).collect();
2453 assert_eq!(p, [80.0, 50.0]);
2454 assert!(
2455 flows
2456 .branches
2457 .iter()
2458 .all(|flow| flow.method == RemovedFlowMethod::Tree)
2459 );
2460 assert!(flows.diagnostics.is_empty(), "{:#?}", flows.diagnostics);
2461 }
2462
2463 #[test]
2464 fn removed_flows_split_a_loop_by_reactance() {
2465 let network = fed_group(vec![
2467 line(2, 3, 0.0, 1e-5),
2468 line(3, 2, 0.0, 3e-5),
2469 jumper(3, 4),
2470 ]);
2471 let merge = network.merge_buses(&PSSE).unwrap();
2472 let flows = merge
2473 .calc_removed_flows(&MergedFlows::new(&[80.0], &[-80.0]))
2474 .unwrap();
2475 let p: Vec<f64> = flows.branches.iter().map(|flow| flow.p_from).collect();
2476 assert!((p[0] - 60.0).abs() < 1e-9, "{p:?}");
2477 assert!(
2478 (p[1] + 20.0).abs() < 1e-9,
2479 "the reversed jumper reads negative: {p:?}"
2480 );
2481 assert!((p[2] - 50.0).abs() < 1e-9, "{p:?}");
2482 assert_eq!(flows.branches[0].method, RemovedFlowMethod::Reactance);
2483 assert_eq!(flows.branches[2].method, RemovedFlowMethod::Tree);
2484 }
2485
2486 #[test]
2487 fn a_loop_of_switches_is_split_by_minimum_norm() {
2488 let mut network = fed_group(vec![jumper(3, 4)]);
2489 network.switches_mut().extend([
2490 Switch::new(BusId(2), BusId(3), true),
2491 Switch::new(BusId(2), BusId(3), true),
2492 ]);
2493 let merge = network.merge_buses(&PSSE).unwrap();
2494 let flows = merge
2495 .calc_removed_flows(&MergedFlows::new(&[80.0], &[-80.0]))
2496 .unwrap();
2497 assert_eq!(flows.branches[0].p_from, 50.0);
2498 for flow in &flows.switches {
2499 assert!((flow.p_from - 40.0).abs() < 1e-9);
2500 assert_eq!(flow.method, RemovedFlowMethod::MinimumNorm);
2501 }
2502 assert_eq!(
2503 codes_of(&flows.diagnostics),
2504 ["CANONICALIZE.MERGE.FLOW_UNDETERMINED"]
2505 );
2506 }
2507
2508 #[test]
2509 fn a_zero_reactance_element_carries_its_parallel_paths_share() {
2510 let mut network = fed_group(vec![jumper(2, 3), jumper(3, 4)]);
2513 network
2514 .switches_mut()
2515 .push(Switch::new(BusId(2), BusId(3), true));
2516 let merge = network.merge_buses(&PSSE).unwrap();
2517 let flows = merge
2518 .calc_removed_flows(&MergedFlows::new(&[80.0], &[-80.0]))
2519 .unwrap();
2520 assert_eq!(flows.branches[0].p_from, 0.0);
2521 assert_eq!(flows.switches[0].p_from, 80.0);
2522 assert_eq!(flows.switches[0].method, RemovedFlowMethod::Tree);
2523 }
2524
2525 #[test]
2526 fn an_unbalanced_group_and_a_wrong_shape_are_reported() {
2527 let mut network = fed_group(vec![jumper(2, 3), jumper(3, 4)]);
2528 network.buses_mut()[0].kind = BusType::Pq;
2529 let merge = network.merge_buses(&PSSE).unwrap();
2530 let flows = merge
2531 .calc_removed_flows(&MergedFlows::new(&[70.0], &[-70.0]))
2532 .unwrap();
2533 assert_eq!(
2534 codes_of(&flows.diagnostics),
2535 ["CANONICALIZE.MERGE.FLOW_RESIDUAL"]
2536 );
2537 let error = merge
2538 .calc_removed_flows(&MergedFlows::new(&[], &[]))
2539 .unwrap_err();
2540 assert_eq!(error.code().code, "CANONICALIZE.MERGE.FLOW_SHAPE_MISMATCH");
2541 }
2542
2543 #[test]
2544 fn out_of_service_elements_carry_no_flow() {
2545 let mut network = fed_group(vec![jumper(2, 3), jumper(3, 4)]);
2546 let mut open = line(2, 4, 0.01, 0.1);
2547 open.in_service = false;
2548 network.branches_mut().push(open);
2549 let merge = network.merge_buses(&PSSE).unwrap();
2550 let flows = merge
2551 .calc_removed_flows(&MergedFlows::new(&[80.0], &[-80.0]))
2552 .unwrap();
2553 assert_eq!(flows.branches[2].method, RemovedFlowMethod::OutOfService);
2554 assert_eq!(flows.branches[2].p_from, 0.0);
2555 }
2556}