use std::borrow::Cow;
use std::collections::HashMap;
use petgraph::graph::UnGraph;
use crate::network::{Branch, BusId, BusType, Generator, Network};
use crate::{Error, Result};
#[derive(Debug, Clone)]
pub struct IndexCore {
bus_id_to_idx: HashMap<BusId, usize>,
pd: Vec<f64>,
qd: Vec<f64>,
gs: Vec<f64>,
bs: Vec<f64>,
}
impl IndexCore {
#[must_use]
pub fn build(net: &Network) -> Self {
let n = net.buses.len();
let bus_id_to_idx: HashMap<BusId, usize> = net
.buses
.iter()
.enumerate()
.map(|(idx, b)| (b.id, idx))
.collect();
debug_assert_eq!(
bus_id_to_idx.len(),
n,
"duplicate bus id in network (run Network::check_references first)"
);
let mut pd = vec![0.0; n];
let mut qd = vec![0.0; n];
for l in &net.loads {
if let Some(&idx) = bus_id_to_idx.get(&l.bus) {
pd[idx] += l.p;
qd[idx] += l.q;
}
}
let mut gs = vec![0.0; n];
let mut bs = vec![0.0; n];
for s in &net.shunts {
if let Some(&idx) = bus_id_to_idx.get(&s.bus) {
gs[idx] += s.g;
bs[idx] += s.b;
}
}
Self {
bus_id_to_idx,
pd,
qd,
gs,
bs,
}
}
}
#[derive(Debug)]
pub struct IndexedNetwork<'n> {
net: Cow<'n, Network>,
core: Cow<'n, IndexCore>,
}
impl<'n> IndexedNetwork<'n> {
#[must_use]
pub fn new(net: &'n Network) -> Self {
let net = net.expand_transformers_3w();
let core = IndexCore::build(&net);
Self {
net,
core: Cow::Owned(core),
}
}
#[must_use]
pub fn with_core(net: &'n Network, core: &'n IndexCore) -> Self {
match net.expand_transformers_3w() {
Cow::Borrowed(net) => Self {
net: Cow::Borrowed(net),
core: Cow::Borrowed(core),
},
Cow::Owned(net) => {
let core = IndexCore::build(&net);
Self {
net: Cow::Owned(net),
core: Cow::Owned(core),
}
}
}
}
#[inline]
pub fn network(&self) -> &Network {
&self.net
}
#[inline]
pub fn n(&self) -> usize {
self.net.buses.len()
}
#[inline]
pub fn base_mva(&self) -> f64 {
self.net.base_mva
}
#[inline]
pub fn per_unit_base(&self) -> f64 {
if self.net.is_normalized() {
1.0
} else {
self.net.base_mva
}
}
#[inline]
pub fn angle_radians(&self, angle: f64) -> f64 {
if self.net.is_normalized() {
angle
} else {
angle.to_radians()
}
}
#[inline]
pub fn name(&self) -> &str {
&self.net.name
}
#[inline]
pub fn branches(&self) -> &[Branch] {
&self.net.branches
}
#[inline]
pub fn generators(&self) -> &[Generator] {
&self.net.generators
}
#[inline]
pub fn bus_index(&self, bus_id: BusId) -> Option<usize> {
self.core.bus_id_to_idx.get(&bus_id).copied()
}
#[inline]
pub fn bus_id(&self, idx: usize) -> BusId {
self.net.buses[idx].id
}
#[inline]
pub fn pd(&self) -> &[f64] {
&self.core.pd
}
#[inline]
pub fn qd(&self) -> &[f64] {
&self.core.qd
}
#[inline]
pub fn gs(&self) -> &[f64] {
&self.core.gs
}
#[inline]
pub fn bs(&self) -> &[f64] {
&self.core.bs
}
pub fn in_service_branches(&self) -> impl Iterator<Item = (usize, &Branch)> {
self.net
.branches
.iter()
.enumerate()
.filter(|(_, b)| b.in_service)
}
pub fn in_service_gens(&self) -> impl Iterator<Item = (usize, &Generator)> {
self.net
.generators
.iter()
.enumerate()
.filter(|(_, g)| g.in_service)
}
pub fn reference_bus_indices(&self) -> Vec<usize> {
self.net
.buses
.iter()
.enumerate()
.filter(|(_, b)| b.kind == BusType::Ref)
.map(|(i, _)| i)
.collect()
}
pub fn reference_bus_index(&self) -> Result<usize> {
match self.reference_bus_indices().as_slice() {
[r] => Ok(*r),
other => Err(Error::ReferenceBusCount { found: other.len() }),
}
}
pub fn to_petgraph(&self) -> UnGraph<usize, usize> {
let mut g = UnGraph::with_capacity(self.n(), self.net.branches.len());
let nodes: Vec<_> = (0..self.n()).map(|i| g.add_node(i)).collect();
for (idx, br) in self.in_service_branches() {
if let (Some(i), Some(j)) = (self.bus_index(br.from), self.bus_index(br.to)) {
g.add_edge(nodes[i], nodes[j], idx);
}
}
g
}
pub fn n_connected_components(&self) -> usize {
petgraph::algo::connected_components(&self.to_petgraph())
}
pub fn connected_component_labels(&self) -> Vec<usize> {
let mut uf = petgraph::unionfind::UnionFind::new(self.n());
for (_, br) in self.in_service_branches() {
if let (Some(i), Some(j)) = (self.bus_index(br.from), self.bus_index(br.to)) {
uf.union(i, j);
}
}
uf.into_labeling()
}
pub fn check_reference_coverage(&self) -> Result<()> {
let labels = self.connected_component_labels();
let mut grounded = vec![false; labels.len()];
for r in self.reference_bus_indices() {
grounded[labels[r]] = true;
}
let ungrounded = (0..labels.len())
.filter(|&i| labels[i] == i && !grounded[i])
.count();
if ungrounded > 0 {
return Err(Error::UngroundedComponent {
components: ungrounded,
});
}
Ok(())
}
pub fn is_radial(&self) -> bool {
let g = self.to_petgraph();
let n_components = petgraph::algo::connected_components(&g);
g.edge_count() == g.node_count().saturating_sub(n_components)
}
pub fn connectivity_report(&self) -> ConnectivityReport {
let g = self.to_petgraph();
let n_components = petgraph::algo::connected_components(&g);
let isolated: Vec<usize> = g
.node_indices()
.filter(|n| g.neighbors(*n).next().is_none())
.map(|n| g[n])
.collect();
ConnectivityReport {
n_buses: self.n(),
n_branches_in_service: self.net.branches.iter().filter(|b| b.in_service).count(),
n_components,
isolated_buses: isolated,
}
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[non_exhaustive]
pub struct ConnectivityReport {
pub n_buses: usize,
pub n_branches_in_service: usize,
pub n_components: usize,
pub isolated_buses: Vec<usize>,
}
impl ConnectivityReport {
#[inline]
pub fn is_single_island(&self) -> bool {
self.n_components == 1 && self.isolated_buses.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::{IndexCore, IndexedNetwork};
use crate::network::{
Bus, BusId, BusType, Extras, Impedance, Load, Network, Shunt, Transformer3W, Winding,
};
fn bus(id: usize, kind: BusType) -> Bus {
Bus {
id: BusId(id),
kind,
vm: 1.0,
va: 0.0,
base_kv: 1.0,
vmax: 1.1,
vmin: 0.9,
evhi: None,
evlo: None,
area: 1,
zone: 1,
name: None,
extras: Extras::new(),
}
}
fn agg_net() -> Network {
let mut net = Network::in_memory(
"agg",
100.0,
vec![bus(1, BusType::Ref), bus(2, BusType::Pq)],
Vec::new(),
);
net.loads.push(Load {
bus: BusId(1),
p: 10.0,
q: 5.0,
voltage_model: None,
in_service: true,
extras: Extras::new(),
});
net.loads.push(Load {
bus: BusId(1),
p: 3.0,
q: 1.0,
voltage_model: None,
in_service: true,
extras: Extras::new(),
});
net.shunts.push(Shunt {
bus: BusId(1),
g: 0.2,
b: 0.4,
in_service: true,
control: None,
extras: Extras::new(),
});
net.shunts.push(Shunt {
bus: BusId(1),
g: 0.1,
b: 0.3,
in_service: true,
control: None,
extras: Extras::new(),
});
net
}
fn assert_aggregates(view: &IndexedNetwork) {
let i = view.bus_index(BusId(1)).unwrap();
assert!((view.pd()[i] - 13.0).abs() < 1e-12);
assert!((view.qd()[i] - 6.0).abs() < 1e-12);
assert!((view.gs()[i] - 0.3).abs() < 1e-12);
assert!((view.bs()[i] - 0.7).abs() < 1e-12);
let j = view.bus_index(BusId(2)).unwrap();
assert!(view.pd()[j].abs() < 1e-12);
assert!(view.gs()[j].abs() < 1e-12);
}
fn three_winding(a: usize, b: usize, c: usize) -> Transformer3W {
let winding = |bus| Winding {
bus: BusId(bus),
tap: 1.0,
shift: 0.0,
nominal_kv: 0.0,
rate_a: 0.0,
rate_b: 0.0,
rate_c: 0.0,
};
let imp = Impedance {
r: 0.0,
x: 0.1,
base_mva: 100.0,
};
Transformer3W {
windings: [winding(a), winding(b), winding(c)],
z: [imp, imp, imp],
star_vm: 1.0,
star_va: 0.0,
mag_g: 0.0,
mag_b: 0.0,
in_service: true,
name: None,
extras: Extras::new(),
}
}
#[test]
fn three_winding_star_lowering_adds_a_grounded_star_bus_with_its_magnetizing_shunt() {
let mut net = Network::in_memory(
"t3w",
100.0,
vec![
bus(1, BusType::Ref),
bus(2, BusType::Pq),
bus(3, BusType::Pq),
],
Vec::new(),
);
let mut t = three_winding(1, 2, 3);
t.mag_b = 0.05; net.transformers_3w.push(t);
let view = IndexedNetwork::new(&net);
assert_eq!(view.n(), 4, "three buses plus the synthetic star point");
assert_eq!(view.n_connected_components(), 1);
view.check_reference_coverage().unwrap();
let star = view.n() - 1;
assert!((view.bs()[star] - 0.05 * 100.0).abs() < 1e-9);
for i in 0..3 {
assert!(view.bs()[i].abs() < 1e-12, "original buses carry no shunt");
}
assert_eq!(net.buses.len(), 3);
assert!(net.branches.is_empty());
assert_eq!(net.transformers_3w.len(), 1);
}
#[test]
fn out_of_service_three_winding_is_not_expanded() {
let mut net = Network::in_memory(
"t3w",
100.0,
vec![
bus(1, BusType::Ref),
bus(2, BusType::Pq),
bus(3, BusType::Pq),
],
Vec::new(),
);
let mut t = three_winding(1, 2, 3);
t.in_service = false;
net.transformers_3w.push(t);
let view = IndexedNetwork::new(&net);
assert_eq!(view.n(), 3);
assert_eq!(view.n_connected_components(), 3);
}
#[test]
fn aggregates_sum_multiple_loads_and_shunts_per_bus() {
let net = agg_net();
assert_aggregates(&IndexedNetwork::new(&net));
}
#[test]
fn with_core_matches_one_shot_view() {
let net = agg_net();
let core = IndexCore::build(&net);
assert_aggregates(&IndexedNetwork::with_core(&net, &core));
}
}