Skip to main content

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}