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: &'n Network,
core: Cow<'n, IndexCore>,
}
impl<'n> IndexedNetwork<'n> {
#[must_use]
pub fn new(net: &'n Network) -> Self {
Self {
net,
core: Cow::Owned(IndexCore::build(net)),
}
}
#[must_use]
pub fn with_core(net: &'n Network, core: &'n IndexCore) -> Self {
Self {
net,
core: Cow::Borrowed(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, Load, Network, Shunt};
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,
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,
in_service: true,
extras: Extras::new(),
});
net.loads.push(Load {
bus: BusId(1),
p: 3.0,
q: 1.0,
in_service: true,
extras: Extras::new(),
});
net.shunts.push(Shunt {
bus: BusId(1),
g: 0.2,
b: 0.4,
in_service: true,
extras: Extras::new(),
});
net.shunts.push(Shunt {
bus: BusId(1),
g: 0.1,
b: 0.3,
in_service: true,
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);
}
#[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));
}
}