powerio_prob/instance/merge.rs
1//! The checked, explicit zero impedance resolution.
2//!
3//! Networks and instances preserve zero impedance branches; a finite matrix
4//! or problem projection refuses them rather than silently skipping rows.
5//! [`merge_zero_impedance_buses`] is the explicit resolution: buses joined by
6//! an in service branch with zero series impedance merge into one electrical
7//! node, and the transformation returns the complete mapping plus diagnostics
8//! for the branch behavior the merge removes. It is
9//! [`BalancedNetwork::merge_buses`] under
10//! [`ZeroImpedanceRule::Exact`](powerio_tx::ZeroImpedanceRule::Exact) with
11//! closed switches left alone.
12
13use std::collections::BTreeMap;
14
15use powerio_core::{Diagnostic, Error};
16use powerio_tx::{
17 BalancedNetwork, BusId, BusMergeRule, MergeSurvivor, RemovalReason, ZeroImpedanceRule,
18};
19
20use crate::diagnostics::codes;
21
22/// What one merge did: which buses now name which surviving bus, and which
23/// branches the merge removed.
24#[derive(Clone, Debug, Default, PartialEq)]
25#[non_exhaustive]
26pub struct ZeroImpedanceMerge {
27 /// Every merged bus to the bus that now carries it. Buses that survived
28 /// unchanged are absent.
29 pub merged_buses: BTreeMap<BusId, BusId>,
30 /// The removed branches, by stable identity (`uid`, else
31 /// `branches:{row}` of the source network): the zero impedance branches
32 /// and any branch whose two buses the merge joined through them.
33 pub removed_branches: Vec<String>,
34}
35
36/// Merge every group of buses joined by in service branches with zero series
37/// impedance (`r == 0` and `x == 0`, self loops and off-nominal transformers
38/// excluded) into that group's smallest bus id, rewriting every element
39/// reference and dropping the merged buses and the zero impedance branches.
40///
41/// The survivor takes the strongest bus kind of its group (reference, then
42/// PV, then PQ). A branch whose two buses the merge joined through other
43/// branches is removed as well, and a removed branch's line charging becomes
44/// a fixed shunt at the survivor.
45///
46/// The flow through a removed branch is no longer a variable of any derived
47/// calculation, and merged buses may have stated different attributes; both
48/// are reported as diagnostics, one per removed branch and one per group whose
49/// base voltages differ. The input network is never mutated.
50/// [`BalancedNetwork::merge_buses`] is the general form, which also merges
51/// closed switches, applies a stated threshold, and recovers removed flows.
52///
53/// # Errors
54/// A zero impedance branch naming a bus the network does not declare.
55pub fn merge_zero_impedance_buses(
56 network: &BalancedNetwork,
57) -> Result<(BalancedNetwork, ZeroImpedanceMerge, Vec<Diagnostic>), Error> {
58 let rule = BusMergeRule::new(false, Some(ZeroImpedanceRule::Exact))
59 .with_survivor(MergeSurvivor::SmallestId);
60 let merge = network
61 .merge_buses(&rule)
62 .map_err(|error| Error::new(error.code(), error.to_string()))?;
63
64 let mut diagnostics = Vec::with_capacity(merge.removed_branches.len());
65 for removed in &merge.removed_branches {
66 if removed.reason == RemovalReason::ZeroImpedance {
67 diagnostics.push(Diagnostic::of(
68 &codes::CANONICALIZE_MERGE_ZERO_IMPEDANCE,
69 format!(
70 "zero impedance branch `{}` between buses {} and {} was merged; its flow is not a variable of any derived calculation",
71 removed.identity, removed.from, removed.to
72 ),
73 ));
74 }
75 }
76 // The per branch findings above replace the merge's one summary finding.
77 diagnostics.extend(
78 merge
79 .diagnostics
80 .iter()
81 .filter(|diagnostic| diagnostic.code() != codes::CANONICALIZE_MERGE_ZERO_IMPEDANCE.code)
82 .cloned(),
83 );
84 let removed_branches = merge
85 .removed_branches
86 .iter()
87 .map(|removed| removed.identity.clone())
88 .collect();
89 Ok((
90 merge.network,
91 ZeroImpedanceMerge {
92 merged_buses: merge.merged_buses,
93 removed_branches,
94 },
95 diagnostics,
96 ))
97}