use std::collections::{BTreeSet, HashMap};
use std::fmt::Write as _;
use crate::diagnostics::{Diagnostic, codes};
use crate::network::{BalancedNetwork, BusId, BusType, Generator};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub enum IslandReferencePolicy {
#[default]
Stated,
PerIsland,
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub struct Island {
pub buses: Vec<BusId>,
pub references: Vec<BusId>,
pub generators: Vec<usize>,
pub hvdc: Vec<usize>,
}
impl Island {
#[must_use]
pub fn is_supplied(&self) -> bool {
!self.generators.is_empty()
}
#[must_use]
pub fn calc_reference_choice(&self, generators: &[Generator]) -> Option<BusId> {
match self.supplied_references(generators).as_slice() {
[bus] => Some(*bus),
[] => largest_generator_bus(generators, &self.generators),
references => kept_reference(references, generators, &self.generators),
}
}
fn supplied_references(&self, generators: &[Generator]) -> Vec<BusId> {
self.references
.iter()
.copied()
.filter(|&bus| {
self.generators
.iter()
.any(|&row| generators[row].bus == bus)
})
.collect()
}
}
#[derive(Clone, Debug, Default, PartialEq)]
#[non_exhaustive]
pub struct IslandPartition {
pub islands: Vec<Island>,
pub isolated: Vec<BusId>,
island_of: HashMap<BusId, usize>,
}
impl IslandPartition {
#[must_use]
pub fn island_of(&self, bus: BusId) -> Option<usize> {
self.island_of.get(&bus).copied()
}
}
#[derive(Clone, Debug, Default, PartialEq)]
#[non_exhaustive]
pub struct IslandReferenceReport {
pub partition: IslandPartition,
pub designated: Vec<BusId>,
pub demoted: Vec<BusId>,
pub de_energized: Vec<BusId>,
pub diagnostics: Vec<Diagnostic>,
}
fn pmax_key(generator: &Generator) -> f64 {
if generator.pmax.is_nan() {
f64::NEG_INFINITY
} else {
generator.pmax
}
}
pub(crate) fn largest_generator_bus(generators: &[Generator], rows: &[usize]) -> Option<BusId> {
rows.iter()
.copied()
.max_by(|&a, &b| {
pmax_key(&generators[a])
.total_cmp(&pmax_key(&generators[b]))
.then(generators[b].bus.cmp(&generators[a].bus))
})
.map(|row| generators[row].bus)
}
pub(crate) fn kept_reference(
references: &[BusId],
generators: &[Generator],
rows: &[usize],
) -> Option<BusId> {
let capacity = |bus: BusId| -> f64 {
rows.iter()
.map(|&row| &generators[row])
.filter(|generator| generator.bus == bus && !generator.pmax.is_nan())
.map(|generator| generator.pmax)
.sum()
};
references
.iter()
.copied()
.max_by(|&a, &b| capacity(a).total_cmp(&capacity(b)).then(b.cmp(&a)))
}
pub(crate) fn bus_list(buses: &[BusId]) -> String {
const SHOWN: usize = 5;
let mut text = buses
.iter()
.take(SHOWN)
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(", ");
if buses.len() > SHOWN {
let _ = write!(text, " and {} more", buses.len() - SHOWN);
}
text
}
impl BalancedNetwork {
#[must_use]
pub fn calc_islands(&self) -> IslandPartition {
let buses = self.buses();
let index_of: HashMap<BusId, usize> = buses
.iter()
.enumerate()
.filter(|(_, bus)| bus.kind != BusType::Isolated)
.map(|(index, bus)| (bus.id, index))
.collect();
let mut sets = petgraph::unionfind::UnionFind::new(buses.len());
let mut join = |a: BusId, b: BusId| {
if let (Some(&i), Some(&j)) = (index_of.get(&a), index_of.get(&b)) {
sets.union(i, j);
}
};
for branch in self.branches().iter().filter(|branch| branch.in_service) {
join(branch.from, branch.to);
}
for switch in self.switches().iter().filter(|switch| switch.closed) {
join(switch.from, switch.to);
}
for transformer in self.transformers_3w().iter().filter(|t| t.in_service) {
let connected: Vec<BusId> = transformer
.windings
.iter()
.zip(transformer.winding_in_service())
.filter(|(_, connected)| *connected)
.map(|(winding, _)| winding.bus)
.collect();
for (i, &a) in connected.iter().enumerate() {
for &b in &connected[i + 1..] {
join(a, b);
}
}
}
let labels = sets.into_labeling();
let mut by_root: HashMap<usize, Island> = HashMap::new();
let mut isolated = Vec::new();
for (index, bus) in buses.iter().enumerate() {
if bus.kind == BusType::Isolated {
isolated.push(bus.id);
continue;
}
let island = by_root.entry(labels[index]).or_insert_with(|| Island {
buses: Vec::new(),
references: Vec::new(),
generators: Vec::new(),
hvdc: Vec::new(),
});
island.buses.push(bus.id);
if bus.kind == BusType::Ref {
island.references.push(bus.id);
}
}
self.attach_generators_and_hvdc(&mut by_root, &labels, &index_of);
let mut islands: Vec<Island> = by_root.into_values().collect();
for island in &mut islands {
island.buses.sort_unstable();
island.references.sort_unstable();
}
islands.sort_by(|a, b| {
b.buses
.len()
.cmp(&a.buses.len())
.then(a.buses[0].cmp(&b.buses[0]))
});
isolated.sort_unstable();
let island_of = islands
.iter()
.enumerate()
.flat_map(|(position, island)| island.buses.iter().map(move |&bus| (bus, position)))
.collect();
IslandPartition {
islands,
isolated,
island_of,
}
}
fn attach_generators_and_hvdc(
&self,
by_root: &mut HashMap<usize, Island>,
labels: &[usize],
index_of: &HashMap<BusId, usize>,
) {
for (row, generator) in self.generators().iter().enumerate() {
if let Some(&index) = index_of.get(&generator.bus)
&& generator.in_service
&& let Some(island) = by_root.get_mut(&labels[index])
{
island.generators.push(row);
}
}
for (row, line) in self.hvdc().iter().enumerate() {
if let (Some(&from), Some(&to)) = (index_of.get(&line.from), index_of.get(&line.to))
&& line.in_service
&& labels[from] != labels[to]
{
for end in [from, to] {
if let Some(island) = by_root.get_mut(&labels[end]) {
island.hvdc.push(row);
}
}
}
}
}
#[allow(clippy::too_many_lines)] pub fn assign_island_references(
&mut self,
policy: IslandReferencePolicy,
) -> IslandReferenceReport {
let partition = self.calc_islands();
if policy == IslandReferencePolicy::Stated {
return IslandReferenceReport {
partition,
..IslandReferenceReport::default()
};
}
let mut designated = Vec::new();
let mut demoted: Vec<(BusId, BusType)> = Vec::new();
let mut dead: BTreeSet<BusId> = BTreeSet::new();
let mut diagnostics = Vec::new();
for island in &partition.islands {
if !island.is_supplied() {
let load: f64 = self
.loads()
.iter()
.filter(|load| load.in_service && island.buses.binary_search(&load.bus).is_ok())
.map(|load| load.p)
.sum();
diagnostics.push(self.unsupplied_island_finding(
island,
&format!(
"was de-energized; its {load} MW of in-service load is no longer served"
),
));
dead.extend(island.buses.iter().copied());
continue;
}
let supplied = island.supplied_references(self.generators());
let unsupplied: Vec<BusId> = island
.references
.iter()
.copied()
.filter(|bus| !supplied.contains(bus))
.collect();
if !unsupplied.is_empty() {
diagnostics.push(Diagnostic::of(
&codes::CANONICALIZE_ISLAND_REFERENCE_DEMOTED,
format!(
"the island of {} bus(es) ({}) states reference bus(es) {} that host no \
in-service generator and cannot balance it; they were demoted to PQ",
island.buses.len(),
bus_list(&island.buses),
bus_list(&unsupplied)
),
));
demoted.extend(unsupplied.iter().map(|&bus| (bus, BusType::Pq)));
}
let Some(kept) = island.calc_reference_choice(self.generators()) else {
continue;
};
match supplied.as_slice() {
[_] => {}
[] => {
diagnostics.push(Diagnostic::of(
&codes::CANONICALIZE_ISLAND_REFERENCE_DESIGNATED,
format!(
"the island of {} bus(es) ({}) states no reference bus that hosts an \
in-service generator; bus {kept} hosts its largest pmax in-service \
generator and was designated the reference",
island.buses.len(),
bus_list(&island.buses)
),
));
designated.push(kept);
}
references => {
let others: Vec<BusId> = references
.iter()
.copied()
.filter(|&bus| bus != kept)
.collect();
diagnostics.push(Diagnostic::of(
&codes::CANONICALIZE_ISLAND_REFERENCE_DEMOTED,
format!(
"the island of {} bus(es) states {} reference buses that host \
generation; bus {kept} hosts the most and stays the reference, and \
bus(es) {} were demoted to PV",
island.buses.len(),
references.len(),
bus_list(&others)
),
));
demoted.extend(others.into_iter().map(|bus| (bus, BusType::Pv)));
}
}
}
if !designated.is_empty() || !demoted.is_empty() {
let designated_set: BTreeSet<BusId> = designated.iter().copied().collect();
let demoted_kind: HashMap<BusId, BusType> = demoted.iter().copied().collect();
for bus in self.buses_mut() {
if designated_set.contains(&bus.id) {
bus.kind = BusType::Ref;
} else if let Some(&kind) = demoted_kind.get(&bus.id) {
bus.kind = kind;
}
}
}
if !dead.is_empty() {
self.de_energize_buses(&dead);
}
let mut demoted: Vec<BusId> = demoted.into_iter().map(|(bus, _)| bus).collect();
demoted.sort_unstable();
IslandReferenceReport {
partition: self.calc_islands(),
designated,
demoted,
de_energized: dead.into_iter().collect(),
diagnostics,
}
}
pub(crate) fn unsupplied_island_finding(&self, island: &Island, outcome: &str) -> Diagnostic {
let size = island.buses.len();
let buses = bus_list(&island.buses);
if island.hvdc.is_empty() {
return Diagnostic::of(
&codes::CANONICALIZE_ISLAND_DE_ENERGIZED,
format!(
"the island of {size} bus(es) ({buses}) has no in-service generator and {outcome}"
),
);
}
let inside = |bus: BusId| island.buses.binary_search(&bus).is_ok();
let mut infeed = 0.0;
let mut lines = Vec::with_capacity(island.hvdc.len());
for &row in &island.hvdc {
let line = &self.hvdc()[row];
infeed += if inside(line.to) { line.pt } else { -line.pf };
lines.push(format!("{}-{}", line.from, line.to));
}
Diagnostic::of(
&codes::CANONICALIZE_ISLAND_HVDC_FED_DE_ENERGIZED,
format!(
"the island of {size} bus(es) ({buses}) has no in-service generator and {outcome}; \
{} in-service HVDC line(s) ({}) schedule {infeed} MW into it, and only \
generators count as supply, so they were taken out of service with it and the \
other island(s) no longer exchange that power. To keep the island, model the \
infeed as a generator at its converter bus",
lines.len(),
lines.join(", ")
),
)
}
pub fn keep_island(&mut self, island: &BTreeSet<BusId>) -> Vec<BusId> {
let others: BTreeSet<BusId> = self
.buses()
.iter()
.filter(|bus| bus.kind != BusType::Isolated && !island.contains(&bus.id))
.map(|bus| bus.id)
.collect();
if !others.is_empty() {
self.de_energize_buses(&others);
}
others.into_iter().collect()
}
pub fn de_energize_buses(&mut self, buses: &BTreeSet<BusId>) {
let at = |bus: BusId| buses.contains(&bus);
if self
.buses()
.iter()
.any(|b| b.kind != BusType::Isolated && at(b.id))
{
for bus in self.buses_mut().iter_mut().filter(|b| at(b.id)) {
bus.kind = BusType::Isolated;
}
}
if self.generators().iter().any(|g| g.in_service && at(g.bus)) {
for generator in self.generators_mut().iter_mut().filter(|g| at(g.bus)) {
generator.in_service = false;
}
}
if self.loads().iter().any(|l| l.in_service && at(l.bus)) {
for load in self.loads_mut().iter_mut().filter(|l| at(l.bus)) {
load.in_service = false;
}
}
if self.shunts().iter().any(|s| s.in_service && at(s.bus)) {
for shunt in self.shunts_mut().iter_mut().filter(|s| at(s.bus)) {
shunt.in_service = false;
}
}
if self
.static_var_compensators()
.iter()
.any(|s| s.in_service && at(s.bus))
{
for svc in self
.static_var_compensators_mut()
.iter_mut()
.filter(|s| at(s.bus))
{
svc.in_service = false;
}
}
if self.storage().iter().any(|s| s.in_service && at(s.bus)) {
for storage in self.storage_mut().iter_mut().filter(|s| at(s.bus)) {
storage.in_service = false;
}
}
let touches = |from: BusId, to: BusId| at(from) || at(to);
if self
.branches()
.iter()
.any(|b| b.in_service && touches(b.from, b.to))
{
for branch in self
.branches_mut()
.iter_mut()
.filter(|b| touches(b.from, b.to))
{
branch.in_service = false;
}
}
if self
.hvdc()
.iter()
.any(|d| d.in_service && touches(d.from, d.to))
{
for line in self.hvdc_mut().iter_mut().filter(|d| touches(d.from, d.to)) {
line.in_service = false;
}
}
let live_after = |t: &crate::network::Transformer3W| -> [bool; 3] {
let mut live = t.winding_in_service();
for (state, winding) in live.iter_mut().zip(&t.windings) {
*state &= !at(winding.bus);
}
live
};
if self
.transformers_3w()
.iter()
.any(|t| t.in_service && live_after(t) != t.winding_in_service())
{
for transformer in self.transformers_3w_mut().iter_mut() {
let live = live_after(transformer);
if live == transformer.winding_in_service() {
continue;
}
if live.iter().filter(|&&on| on).count() < 2 {
transformer.in_service = false;
} else {
transformer.set_winding_in_service(live);
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::network::{Branch, Bus, Hvdc, Impedance, Load, Switch, Transformer3W, Winding};
use crate::{ClosedSwitchPolicy, IslandReferencePolicy, NormalizeOptions, NormalizeSettings};
fn generator(bus: usize, pmax: f64) -> Generator {
let mut generator = Generator::new(BusId(bus));
generator.pmax = pmax;
generator.pg = pmax / 2.0;
generator
}
fn three_islands() -> BalancedNetwork {
let mut buses: Vec<Bus> = (1..=9)
.map(|id| Bus::new(BusId(id), BusType::Pq, 230.0))
.collect();
buses[0].kind = BusType::Ref;
buses[7].kind = BusType::Isolated;
let line = |from, to| Branch::new(BusId(from), BusId(to), 0.01, 0.1);
let mut net = BalancedNetwork::in_memory(
"islands",
100.0,
buses,
vec![line(1, 2), line(2, 9), line(3, 4), line(6, 7), line(7, 8)],
);
net.switches_mut()
.push(Switch::new(BusId(4), BusId(5), true));
net.generators_mut()
.extend([generator(1, 200.0), generator(4, 50.0), generator(5, 80.0)]);
net.loads_mut().extend([
Load::new(BusId(2), 60.0, 0.0),
Load::new(BusId(3), 40.0, 0.0),
Load::new(BusId(7), 25.0, 0.0),
]);
let mut tie = Hvdc::new(BusId(1), BusId(6));
tie.pf = 30.0;
tie.pt = 29.0;
let inside = Hvdc::new(BusId(1), BusId(2));
net.hvdc_mut().extend([tie, inside]);
net
}
fn kind(net: &BalancedNetwork, id: usize) -> BusType {
net.buses().iter().find(|b| b.id == BusId(id)).unwrap().kind
}
fn codes(diagnostics: &[Diagnostic]) -> Vec<&str> {
diagnostics.iter().map(Diagnostic::code).collect()
}
#[test]
fn islands_follow_branches_closed_switches_and_not_hvdc() {
let net = three_islands();
let partition = net.calc_islands();
let buses: Vec<Vec<usize>> = partition
.islands
.iter()
.map(|island| island.buses.iter().map(|b| b.0).collect())
.collect();
assert_eq!(buses, [vec![1, 2, 9], vec![3, 4, 5], vec![6, 7]]);
assert_eq!(partition.isolated, [BusId(8)]);
assert_eq!(partition.islands[0].references, [BusId(1)]);
assert_eq!(partition.islands[1].generators, [1, 2]);
assert!(!partition.islands[2].is_supplied());
assert_eq!(partition.island_of(BusId(5)), Some(1));
assert_eq!(partition.island_of(BusId(8)), None);
assert_eq!(partition.islands[0].hvdc, [0]);
assert_eq!(partition.islands[1].hvdc, [] as [usize; 0]);
assert_eq!(partition.islands[2].hvdc, [0]);
}
#[test]
fn a_three_winding_transformer_joins_its_windings_while_in_service() {
let buses = (1..=3)
.map(|id| Bus::new(BusId(id), BusType::Pq, 230.0))
.collect();
let mut net = BalancedNetwork::in_memory("t3w", 100.0, buses, Vec::new());
net.transformers_3w_mut().push(Transformer3W::new(
[1, 2, 3].map(|bus| Winding::new(BusId(bus))),
[Impedance::new(0.0, 0.1, 100.0); 3],
));
assert_eq!(net.calc_islands().islands.len(), 1);
net.transformers_3w_mut()[0].in_service = false;
assert_eq!(net.calc_islands().islands.len(), 3);
}
#[test]
fn a_winding_on_an_isolated_bus_leaves_the_other_two_joined() {
let mut buses: Vec<Bus> = (1..=3)
.map(|id| Bus::new(BusId(id), BusType::Pq, 230.0))
.collect();
buses[0].kind = BusType::Ref;
buses[1].kind = BusType::Isolated;
let mut net = BalancedNetwork::in_memory("t3w", 100.0, buses, Vec::new());
net.transformers_3w_mut().push(Transformer3W::new(
[1, 2, 3].map(|bus| Winding::new(BusId(bus))),
[Impedance::new(0.0, 0.1, 100.0); 3],
));
net.generators_mut().push(generator(1, 100.0));
net.loads_mut().push(Load::new(BusId(3), 10.0, 0.0));
let partition = net.calc_islands();
assert_eq!(partition.islands.len(), 1);
assert_eq!(partition.islands[0].buses, [BusId(1), BusId(3)]);
let settings =
NormalizeSettings::default().with_island_references(IslandReferencePolicy::PerIsland);
let (normalized, _) = net.to_normalized_with(&settings).unwrap();
assert_eq!(normalized.network.buses().len(), 2);
let transformer = &normalized.network.transformers_3w()[0];
assert_eq!(transformer.winding_in_service(), [true, false, true]);
assert!(transformer.in_service);
assert_eq!(normalized.network.calc_islands().islands.len(), 1);
let found = codes(&normalized.diagnostics);
assert!(found.contains(&"CANONICALIZE.NORMALIZE.WINDING_TAKEN_OUT"));
assert!(!found.contains(&"CANONICALIZE.ISLAND.DE_ENERGIZED"));
crate::IndexedNetwork::new(&normalized.network)
.check_reference_coverage()
.unwrap();
normalized.network.validate().unwrap();
}
#[test]
fn de_energizing_takes_out_the_windings_on_the_dead_island() {
let mut net = three_islands();
net.transformers_3w_mut().push(Transformer3W::new(
[6, 7, 6].map(|bus| Winding::new(BusId(bus))),
[Impedance::new(0.0, 0.1, 100.0); 3],
));
let mut partial = Transformer3W::new(
[3, 4, 7].map(|bus| Winding::new(BusId(bus))),
[Impedance::new(0.0, 0.1, 100.0); 3],
);
partial.set_winding_in_service([true, true, false]);
net.transformers_3w_mut().push(partial);
net.assign_island_references(IslandReferencePolicy::PerIsland);
assert!(
!net.transformers_3w()[0].in_service,
"nothing left to couple"
);
let partial = &net.transformers_3w()[1];
assert!(partial.in_service, "its live windings stay");
assert_eq!(partial.winding_in_service(), [true, true, false]);
net.validate().unwrap();
}
#[test]
fn each_island_gets_one_reference_and_an_unsupplied_one_is_de_energized() {
let mut net = three_islands();
let unchanged = net.assign_island_references(IslandReferencePolicy::Stated);
assert_eq!(unchanged.diagnostics.as_slice(), []);
assert_eq!(kind(&net, 5), BusType::Pq);
let report = net.assign_island_references(IslandReferencePolicy::PerIsland);
assert_eq!(report.designated, [BusId(5)]);
assert_eq!(kind(&net, 1), BusType::Ref);
assert_eq!(kind(&net, 5), BusType::Ref);
assert_eq!(report.de_energized, [BusId(6), BusId(7)]);
assert_eq!(kind(&net, 6), BusType::Isolated);
assert!(!net.loads()[2].in_service);
assert!(!net.branches()[3].in_service);
assert!(!net.branches()[4].in_service, "the branch onto bus 8");
assert!(!net.hvdc()[0].in_service);
assert!(net.hvdc()[1].in_service, "the line inside island A");
assert!(net.branches()[0].in_service && net.loads()[0].in_service);
assert_eq!(
codes(&report.diagnostics),
[
"CANONICALIZE.ISLAND.REFERENCE_DESIGNATED",
"CANONICALIZE.ISLAND.HVDC_FED_DE_ENERGIZED"
]
);
let message = report.diagnostics[1].message();
assert!(message.contains("25 MW of in-service load"), "{message}");
assert!(
message.contains("(1-6) schedule 29 MW into it"),
"{message}"
);
assert_eq!(report.partition.islands.len(), 2);
assert!(
report
.partition
.islands
.iter()
.all(|island| island.references.len() == 1)
);
net.validate().unwrap();
let again = net.assign_island_references(IslandReferencePolicy::PerIsland);
assert_eq!(again.diagnostics.as_slice(), []);
}
#[test]
fn of_two_references_in_one_island_the_larger_source_stays() {
let mut net = three_references();
let report = net.assign_island_references(IslandReferencePolicy::PerIsland);
assert_eq!(report.demoted, [BusId(1), BusId(3)]);
assert_eq!(kind(&net, 2), BusType::Ref);
assert_eq!(kind(&net, 1), BusType::Pv, "it hosts a generator");
assert_eq!(kind(&net, 3), BusType::Pq);
assert_eq!(
codes(&report.diagnostics),
[
"CANONICALIZE.ISLAND.REFERENCE_DEMOTED",
"CANONICALIZE.ISLAND.REFERENCE_DEMOTED"
]
);
}
fn lone_generator_less_reference() -> BalancedNetwork {
let mut buses: Vec<Bus> = (1..=4)
.map(|id| Bus::new(BusId(id), BusType::Pq, 230.0))
.collect();
buses[0].kind = BusType::Ref;
let line = |from, to| Branch::new(BusId(from), BusId(to), 0.01, 0.1);
let mut net = BalancedNetwork::in_memory(
"generator-less-reference",
100.0,
buses,
vec![line(1, 2), line(2, 3), line(3, 4)],
);
net.generators_mut()
.extend([generator(3, 50.0), generator(4, 120.0)]);
net.loads_mut().push(Load::new(BusId(2), 60.0, 0.0));
net
}
fn three_references() -> BalancedNetwork {
let buses = (1..=3)
.map(|id| Bus::new(BusId(id), BusType::Ref, 230.0))
.collect();
let line = |from, to| Branch::new(BusId(from), BusId(to), 0.01, 0.1);
let mut net =
BalancedNetwork::in_memory("refs", 100.0, buses, vec![line(1, 2), line(2, 3)]);
net.generators_mut()
.extend([generator(1, 100.0), generator(2, 300.0)]);
net
}
#[test]
fn a_lone_generator_less_reference_yields_to_the_largest_generator() {
let mut net = lone_generator_less_reference();
let island = &net.calc_islands().islands[0];
assert_eq!(island.references, [BusId(1)]);
assert_eq!(
island.calc_reference_choice(net.generators()),
Some(BusId(4))
);
let report = net.assign_island_references(IslandReferencePolicy::PerIsland);
assert_eq!(report.demoted, [BusId(1)]);
assert_eq!(report.designated, [BusId(4)]);
assert_eq!(report.de_energized, []);
assert_eq!(kind(&net, 1), BusType::Pq);
assert_eq!(kind(&net, 4), BusType::Ref);
assert_eq!(
kind(&net, 3),
BusType::Pq,
"the smaller generator stays put"
);
assert_eq!(
codes(&report.diagnostics),
[
"CANONICALIZE.ISLAND.REFERENCE_DEMOTED",
"CANONICALIZE.ISLAND.REFERENCE_DESIGNATED"
]
);
assert_eq!(report.partition.islands[0].references, [BusId(4)]);
net.validate().unwrap();
let again = net.assign_island_references(IslandReferencePolicy::PerIsland);
assert_eq!(again.diagnostics.as_slice(), []);
let mut stated = lone_generator_less_reference();
let report = stated.assign_island_references(IslandReferencePolicy::Stated);
assert_eq!(report.diagnostics.as_slice(), []);
assert_eq!(kind(&stated, 1), BusType::Ref);
}
#[test]
fn equal_generators_go_to_the_smallest_bus_id() {
let mut net = lone_generator_less_reference();
net.generators_mut().push(generator(2, 120.0));
let report = net.assign_island_references(IslandReferencePolicy::PerIsland);
assert_eq!(report.designated, [BusId(2)]);
}
#[test]
fn raw_and_normalized_per_island_references_agree() {
let references = |network: &BalancedNetwork| -> Vec<BusId> {
let mut ids: Vec<BusId> = network
.buses()
.iter()
.filter(|bus| bus.kind == BusType::Ref)
.map(|bus| bus.id)
.collect();
ids.sort_unstable();
ids
};
let settings = NormalizeSettings::default()
.with_island_references(IslandReferencePolicy::PerIsland)
.with_closed_switches(ClosedSwitchPolicy::Keep);
let fixtures = [
("three islands", three_islands(), vec![1, 5]),
(
"dead tertiary",
line_with_a_tertiary(BusType::Isolated),
vec![1],
),
("live tertiary", line_with_a_tertiary(BusType::Pq), vec![1]),
(
"generator-less reference",
lone_generator_less_reference(),
vec![4],
),
("three references", three_references(), vec![2]),
];
for (name, net, expected) in fixtures {
let expected: Vec<BusId> = expected.into_iter().map(BusId).collect();
let (normalized, _) = net.to_normalized_with(&settings).unwrap();
assert_eq!(
references(&normalized.network),
expected,
"{name}: normalized"
);
let mut raw = net.clone();
raw.assign_island_references(IslandReferencePolicy::PerIsland);
assert_eq!(references(&raw), expected, "{name}: raw");
let choices: Vec<BusId> = net
.calc_islands()
.islands
.iter()
.filter_map(|island| island.calc_reference_choice(net.generators()))
.collect();
let mut choices = choices;
choices.sort_unstable();
assert_eq!(choices, expected, "{name}: calc_reference_choice");
}
}
#[test]
fn normalization_per_island_references_every_island_and_drops_the_unsupplied() {
let net = three_islands();
let stated = net
.to_normalized_with_options(&NormalizeOptions::default())
.unwrap();
assert_eq!(
stated.network.buses().len(),
7,
"the typed isolated bus goes, and the closed switch merges bus 5 into bus 4"
);
let references: Vec<BusId> = stated
.network
.buses()
.iter()
.filter(|bus| bus.kind == BusType::Ref)
.map(|bus| bus.id)
.collect();
assert_eq!(references, [BusId(1)]);
assert!(
codes(&stated.diagnostics).contains(&"CANONICALIZE.NORMALIZE.GENERATOR_BUS_RETYPED")
);
let settings =
NormalizeSettings::default().with_island_references(IslandReferencePolicy::PerIsland);
let references = |network: &BalancedNetwork| -> Vec<BusId> {
network
.buses()
.iter()
.filter(|bus| bus.kind == BusType::Ref)
.map(|bus| bus.id)
.collect()
};
let (kept, _) = net
.to_normalized_with(&settings.with_closed_switches(ClosedSwitchPolicy::Keep))
.unwrap();
let ids: Vec<usize> = kept.network.buses().iter().map(|b| b.id.0).collect();
assert_eq!(ids, [1, 2, 3, 4, 5, 9]);
assert_eq!(references(&kept.network), [BusId(1), BusId(5)]);
let (per_island, _) = net.to_normalized_with(&settings).unwrap();
let ids: Vec<usize> = per_island.network.buses().iter().map(|b| b.id.0).collect();
assert_eq!(ids, [1, 2, 3, 4, 9]);
assert_eq!(references(&per_island.network), [BusId(1), BusId(4)]);
assert_eq!(
per_island.network.hvdc().len(),
1,
"only the line inside island A"
);
let found = codes(&per_island.diagnostics);
assert!(found.contains(&"CANONICALIZE.ISLAND.HVDC_FED_DE_ENERGIZED"));
assert!(!found.contains(&"CANONICALIZE.ISLAND.DE_ENERGIZED"));
assert!(found.contains(&"CANONICALIZE.ISLAND.REFERENCE_DESIGNATED"));
crate::IndexedNetwork::new(&per_island.network)
.check_reference_coverage()
.unwrap();
}
fn line_with_a_tertiary(tertiary: BusType) -> BalancedNetwork {
let mut buses: Vec<Bus> = (1..=5)
.map(|id| Bus::new(BusId(id), BusType::Pq, 230.0))
.collect();
buses[0].kind = BusType::Ref;
buses[4].kind = tertiary;
let line = |from, to| Branch::new(BusId(from), BusId(to), 0.01, 0.1);
let mut net =
BalancedNetwork::in_memory("tertiary", 100.0, buses, vec![line(1, 2), line(2, 3)]);
net.transformers_3w_mut().push(Transformer3W::new(
[2, 4, 5].map(|bus| Winding::new(BusId(bus))),
[Impedance::new(0.0, 0.1, 100.0); 3],
));
net.generators_mut().push(generator(1, 100.0));
net
}
#[test]
fn an_island_joined_through_a_dead_tertiary_carves_out_whole() {
let net = line_with_a_tertiary(BusType::Isolated);
let partition = net.calc_islands();
assert_eq!(partition.islands.len(), 1);
assert_eq!(
partition.islands[0].buses,
[BusId(1), BusId(2), BusId(3), BusId(4)]
);
assert_eq!(partition.isolated, [BusId(5)]);
let island: BTreeSet<BusId> = partition.islands[0].buses.iter().copied().collect();
let mut sub = net.subset_buses(&island).network;
assert_eq!(sub.transformers_3w().len(), 1, "the transformer is kept");
let stub = sub.buses().iter().find(|b| b.id == BusId(5)).unwrap();
assert_eq!(stub.kind, BusType::Isolated);
assert_eq!(
stub.extras.get("tie_bus"),
Some(&serde_json::Value::Bool(true))
);
sub.validate().unwrap();
let again = sub.calc_islands();
assert_eq!(again.islands.len(), 1);
assert_eq!(again.islands[0].buses, partition.islands[0].buses);
let report = sub.assign_island_references(IslandReferencePolicy::PerIsland);
assert_eq!(report.diagnostics.as_slice(), []);
}
#[test]
fn a_winding_stated_out_of_service_joins_nothing() {
let mut net = line_with_a_tertiary(BusType::Pq);
net.transformers_3w_mut()[0].extras.insert(
"winding_in_service".into(),
serde_json::json!([true, true, false]),
);
let partition = net.calc_islands();
assert_eq!(partition.islands.len(), 2);
assert_eq!(partition.islands[1].buses, [BusId(5)]);
let island: BTreeSet<BusId> = partition.islands[0].buses.iter().copied().collect();
let sub = net.subset_buses(&island).network;
assert_eq!(sub.transformers_3w().len(), 1);
assert_eq!(sub.calc_islands().islands.len(), 1);
sub.validate().unwrap();
}
#[test]
fn one_island_carves_out_with_its_elements() {
let net = three_islands();
let partition = net.calc_islands();
let island: BTreeSet<BusId> = partition.islands[1].buses.iter().copied().collect();
let cut = net.subset_buses(&island);
assert_eq!(cut.branch_rows, [None, None, Some(0), None, None]);
assert_eq!(cut.switch_rows, [Some(0)]);
assert_eq!(cut.generator_rows, [None, Some(0), Some(1)]);
assert_eq!(cut.load_rows, [None, Some(0), None]);
assert_eq!(cut.hvdc_rows, [None, None]);
let sub = cut.network;
assert_eq!(sub.buses().len(), 3);
assert_eq!(sub.branches().len(), 1);
assert_eq!(sub.switches().len(), 1);
assert_eq!(sub.generators().len(), 2);
assert_eq!(sub.loads().len(), 1);
assert_eq!(sub.hvdc().as_slice(), []);
sub.validate().unwrap();
}
#[test]
fn island_cut_row_maps_agree_with_the_cut_network() {
let fixtures = [
three_islands(),
line_with_a_tertiary(BusType::Isolated),
line_with_a_tertiary(BusType::Pq),
];
for net in fixtures {
for island in net.calc_islands().islands {
let island: BTreeSet<BusId> = island.buses.iter().copied().collect();
let cut = net.subset_buses(&island);
let kept: BTreeSet<BusId> = cut.network.buses().iter().map(|b| b.id).collect();
assert!(island.is_subset(&kept));
macro_rules! check {
($map:ident, $table:ident, $kept:expr) => {{
let source = net.$table();
let result = cut.network.$table();
assert_eq!(cut.$map.len(), source.len(), stringify!($map));
let rows: Vec<usize> = cut.$map.iter().flatten().copied().collect();
assert_eq!(rows, (0..result.len()).collect::<Vec<_>>());
for (row, target) in cut.$map.iter().enumerate() {
let element = &source[row];
#[allow(clippy::redundant_closure_call)]
let expected = ($kept)(element);
assert_eq!(
target.is_some(),
expected,
"{} row {row}",
stringify!($map)
);
if let Some(target) = target {
assert_eq!(&result[*target], element);
}
}
}};
}
for (row, target) in cut.bus_rows.iter().enumerate() {
let id = net.buses()[row].id;
assert_eq!(target.is_some(), kept.contains(&id));
if let Some(target) = target {
assert_eq!(cut.network.buses()[*target].id, id);
}
}
let at = |bus: &BusId| island.contains(bus);
let spans = |from: &BusId, to: &BusId| kept.contains(from) && kept.contains(to);
check!(load_rows, loads, |e: &Load| at(&e.bus));
check!(generator_rows, generators, |e: &Generator| at(&e.bus));
check!(branch_rows, branches, |e: &Branch| spans(&e.from, &e.to));
check!(switch_rows, switches, |e: &Switch| spans(&e.from, &e.to));
check!(hvdc_rows, hvdc, |e: &Hvdc| spans(&e.from, &e.to));
check!(transformer_3w_rows, transformers_3w, |e: &Transformer3W| e
.windings
.iter()
.all(|w| kept.contains(&w.bus))
&& e.windings.iter().any(|w| at(&w.bus)));
assert_eq!(cut.shunt_rows.len(), net.shunts().len());
assert_eq!(cut.storage_rows.len(), net.storage().len());
assert_eq!(
cut.static_var_compensator_rows.len(),
net.static_var_compensators().len()
);
}
}
}
#[test]
fn de_energizing_buses_keeps_every_row() {
let mut net = three_islands();
let rows = |net: &BalancedNetwork| {
[
net.buses().len(),
net.branches().len(),
net.switches().len(),
net.generators().len(),
net.loads().len(),
net.hvdc().len(),
]
};
let before = rows(&net);
net.de_energize_buses(&[BusId(3), BusId(4), BusId(5), BusId(99)].into());
assert_eq!(rows(&net), before, "no row is added or removed");
for bus in [3, 4, 5] {
assert_eq!(kind(&net, bus), BusType::Isolated);
}
assert_eq!(kind(&net, 2), BusType::Pq);
assert!(!net.branches()[2].in_service, "the line 3-4");
assert!(net.switches()[0].closed, "switches keep their state");
assert!(net.generators()[0].in_service);
assert!(!net.generators()[1].in_service && !net.generators()[2].in_service);
assert!(!net.loads()[1].in_service && net.loads()[0].in_service);
let partition = net.calc_islands();
assert_eq!(partition.island_of(BusId(4)), None);
assert_eq!(partition.islands.len(), 2);
net.validate().unwrap();
}
#[test]
fn keeping_one_island_de_energizes_the_others_in_place() {
let mut net = three_islands();
let partition = net.calc_islands();
let island: BTreeSet<BusId> = partition.islands[1].buses.iter().copied().collect();
let cut = net.subset_buses(&island);
let dead = net.keep_island(&island);
assert_eq!(dead, [1, 2, 6, 7, 9].map(BusId));
let after = net.calc_islands();
assert_eq!(after.islands.len(), 1);
assert_eq!(after.islands[0].buses, partition.islands[1].buses);
for (row, target) in cut.branch_rows.iter().enumerate() {
assert_eq!(net.branches()[row].in_service, target.is_some());
}
for (row, target) in cut.generator_rows.iter().enumerate() {
assert_eq!(net.generators()[row].in_service, target.is_some());
}
for (row, target) in cut.load_rows.iter().enumerate() {
assert_eq!(net.loads()[row].in_service, target.is_some());
}
for (row, target) in cut.hvdc_rows.iter().enumerate() {
assert_eq!(net.hvdc()[row].in_service, target.is_some());
}
assert_eq!(
net.keep_island(&island),
[],
"a second call changes nothing"
);
let report = net.assign_island_references(IslandReferencePolicy::PerIsland);
assert_eq!(report.de_energized, []);
net.validate().unwrap();
}
}