use std::collections::BTreeSet;
use super::sub::SubsystemSet;
use super::{
AutomaticOrder, AutomaticSpec, AutomaticTarget, ContingencyAction, ContingencyCase,
ContingencySet, PsseEquipmentIndex, SkipRule,
};
use crate::diagnostics::{Diagnostic, codes};
use crate::network::{BalancedNetwork, BusId, Transformer3W};
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct Expanded {
pub set: ContingencySet,
pub diagnostics: Vec<Diagnostic>,
}
impl ContingencySet {
#[must_use]
pub fn expand(&self, net: &BalancedNetwork, subsystems: &SubsystemSet) -> Expanded {
self.expand_with(&PsseEquipmentIndex::new(net), subsystems)
}
#[must_use]
pub fn expand_with(
&self,
index: &PsseEquipmentIndex<'_>,
subsystems: &SubsystemSet,
) -> Expanded {
let net = index.network();
let mut diagnostics = Vec::new();
let mut kept = Vec::new();
let mut generated = Vec::new();
for spec in &self.automatic {
let Some(subsystem) = subsystems.get(&spec.subsystem) else {
diagnostics.push(Diagnostic::of(
&codes::BUILD_CON_SUBSYSTEM_UNKNOWN,
format!(
"{}: the subsystem set states no subsystem '{}'",
describe(spec),
spec.subsystem
),
));
kept.push(spec.clone());
continue;
};
let buses = subsystem.select_buses(net);
let singles = single_cases(net, index, spec, &buses, &self.skips);
let needed = match spec.order {
AutomaticOrder::Single => 1,
AutomaticOrder::Double => 2,
};
let eligible = singles.len();
if eligible < needed {
let plural = if eligible == 1 { "element" } else { "elements" };
diagnostics.push(Diagnostic::of(
&codes::BUILD_CON_SPECIFICATION_EMPTY,
format!(
"{}: subsystem '{}' holds {eligible} in service {plural} this specification names, and its order needs {needed}",
describe(spec),
spec.subsystem
),
));
}
match spec.order {
AutomaticOrder::Single => generated.extend(singles),
AutomaticOrder::Double => generated.extend(double_cases(&singles)),
}
}
let mut cases = self.cases.clone();
cases.extend(generated);
let skips = if kept.is_empty() && !self.automatic.is_empty() {
Vec::new()
} else {
self.skips.clone()
};
Expanded {
set: ContingencySet {
header: self.header.clone(),
cases,
automatic: kept,
skips,
retained: self.retained.clone(),
},
diagnostics,
}
}
}
fn describe(spec: &AutomaticSpec) -> String {
let order = match spec.order {
AutomaticOrder::Single => "SINGLE",
AutomaticOrder::Double => "DOUBLE",
};
let target = match spec.target {
AutomaticTarget::Branch => "BRANCH",
AutomaticTarget::Unit => "UNIT",
AutomaticTarget::Tie => "TIE",
};
format!("{order} {target}")
}
fn single_cases(
net: &BalancedNetwork,
index: &PsseEquipmentIndex,
spec: &AutomaticSpec,
buses: &BTreeSet<BusId>,
skips: &[SkipRule],
) -> Vec<ContingencyCase> {
let mut cases = Vec::new();
match spec.target {
AutomaticTarget::Branch | AutomaticTarget::Tie => {
let tie = spec.target == AutomaticTarget::Tie;
for (row, branch) in net.branches().iter().enumerate() {
let inside = usize::from(buses.contains(&branch.from))
+ usize::from(buses.contains(&branch.to));
let wanted = if tie { inside == 1 } else { inside == 2 };
if !branch.in_service || !wanted {
continue;
}
let circuit = index.circuit_ids()[row].trim();
if skipped(skips, branch.from, branch.to, circuit) {
continue;
}
cases.push(ContingencyCase {
name: format!("L_{}_{}_{circuit}", branch.from.0, branch.to.0),
actions: vec![ContingencyAction::OpenBranch {
from: branch.from,
to: branch.to,
circuit: circuit.to_owned(),
}],
});
}
if spec.low_voltage_3w && !tie {
cases.extend(three_winding_cases(net, index, buses));
}
}
AutomaticTarget::Unit => {
for (row, generator) in net.generators().iter().enumerate() {
if !generator.in_service || !buses.contains(&generator.bus) {
continue;
}
let id = index.machine_ids()[row].trim();
cases.push(ContingencyCase {
name: format!("G_{}_{id}", generator.bus.0),
actions: vec![ContingencyAction::RemoveMachine {
bus: generator.bus,
id: id.to_owned(),
}],
});
}
}
}
cases
}
fn three_winding_cases(
net: &BalancedNetwork,
index: &PsseEquipmentIndex,
buses: &BTreeSet<BusId>,
) -> Vec<ContingencyCase> {
let mut cases = Vec::new();
for (row, transformer) in net.transformers_3w().iter().enumerate() {
if !transformer.in_service || !buses.contains(&low_voltage_bus(net, index, transformer)) {
continue;
}
let circuit = index.transformer_3w_ids()[row].trim();
let terminals = [
transformer.windings[0].bus,
transformer.windings[1].bus,
transformer.windings[2].bus,
];
cases.push(ContingencyCase {
name: format!(
"T_{}_{}_{}_{circuit}",
terminals[0].0, terminals[1].0, terminals[2].0
),
actions: vec![ContingencyAction::OpenThreeWinding {
buses: terminals,
circuit: circuit.to_owned(),
}],
});
}
cases
}
fn double_cases(singles: &[ContingencyCase]) -> Vec<ContingencyCase> {
let mut cases = Vec::new();
for (position, first) in singles.iter().enumerate() {
for second in &singles[position + 1..] {
let mut actions = first.actions.clone();
actions.extend(second.actions.iter().cloned());
cases.push(ContingencyCase {
name: format!("{}+{}", first.name, second.name),
actions,
});
}
}
cases
}
fn skipped(skips: &[SkipRule], from: BusId, to: BusId, circuit: &str) -> bool {
skips.iter().any(|rule| {
rule.circuit.trim() == circuit
&& ((rule.from == from && rule.to == to) || (rule.from == to && rule.to == from))
})
}
pub(super) fn low_voltage_bus(
net: &BalancedNetwork,
index: &PsseEquipmentIndex,
transformer: &Transformer3W,
) -> BusId {
let kv_of = |winding: &crate::network::Winding| {
if winding.nominal_kv > 0.0 {
return winding.nominal_kv;
}
index
.bus_row(winding.bus)
.map_or(0.0, |row| net.buses()[row].base_kv)
};
let mut lowest = &transformer.windings[0];
for winding in &transformer.windings[1..] {
if kv_of(winding) < kv_of(lowest) {
lowest = winding;
}
}
lowest.bus
}