use std::borrow::Cow;
use std::collections::BTreeMap;
use std::sync::Arc;
use powerio_core::ComponentId;
use crate::diagnostics::{Diagnostic, codes};
use crate::format::psse::{
detailed_source_property, quoted_circuit_id, quoted_device_id, transformer_3w_id,
};
use crate::network::{BalancedNetwork, BusId, BusType};
use super::{ContingencyAction, ContingencySet, MAX_READER_NOTES};
const BUS: &str = "bus";
const LOAD: &str = "load";
const SHUNT: &str = "shunt";
const GENERATOR: &str = "generator";
const BRANCH: &str = "branch";
const SWITCH: &str = "switch";
const TRANSFORMER_3W: &str = "transformer_3w";
#[derive(Debug, Clone)]
pub struct PsseEquipmentIndex<'n> {
net: &'n BalancedNetwork,
tables: Cow<'n, EquipmentTables>,
}
#[derive(Debug, Clone)]
struct EquipmentTables {
machine_ids: Vec<String>,
circuit_ids: Vec<String>,
switch_ids: Vec<String>,
transformer_3w_ids: Vec<String>,
bus_rows: BTreeMap<BusId, usize>,
branch_rows: BranchRows,
switch_rows: SwitchRows,
machine_rows: MachineRows,
fixed_shunt_rows: DeviceRows,
switched_shunt_rows: BTreeMap<BusId, Vec<usize>>,
load_rows: DeviceRows,
transformer_3w_rows: Transformer3wRows,
names: super::tara::NameIndex,
}
fn sorted_triple(buses: [BusId; 3]) -> [BusId; 3] {
let mut sorted = buses;
sorted.sort_unstable();
sorted
}
type DeviceRows = BTreeMap<BusId, Vec<(String, usize)>>;
type BranchRows = BTreeMap<(BusId, BusId, bool, String), Vec<usize>>;
type MachineRows = BTreeMap<(BusId, String), usize>;
type SwitchRows = BTreeMap<(BusId, BusId, String), Vec<usize>>;
type Transformer3wRows = BTreeMap<[BusId; 3], Vec<(String, usize)>>;
fn machine_index(net: &BalancedNetwork, sanitized: &mut usize) -> (Vec<String>, MachineRows) {
let mut ids = Vec::with_capacity(net.generators().len());
let mut rows = BTreeMap::new();
let mut used = BTreeMap::new();
for (row, generator) in net.generators().iter().enumerate() {
let preferred =
detailed_source_property(net, GENERATOR, generator.uid.as_deref(), "psse_eqid")
.filter(|id| !id.is_empty());
let id = quoted_circuit_id(preferred, generator.bus, &mut used, sanitized);
rows.insert((generator.bus, id.trim().to_owned()), row);
ids.push(id);
}
(ids, rows)
}
fn branch_index(net: &BalancedNetwork, sanitized: &mut usize) -> (Vec<String>, BranchRows) {
let mut ids = vec![String::new(); net.branches().len()];
let mut rows: BranchRows = BTreeMap::new();
let mut line_ids = BTreeMap::new();
let mut transformer_ids = BTreeMap::new();
for transformers in [false, true] {
for (row, branch) in net.branches().iter().enumerate() {
if branch.is_transformer() != transformers {
continue;
}
let retained = transformers
.then(|| {
detailed_source_property(net, "transformer", branch.uid.as_deref(), "psse_eqid")
})
.flatten();
let preferred = branch
.extras
.get("id")
.and_then(serde_json::Value::as_str)
.or(retained);
let used = if transformers {
&mut transformer_ids
} else {
&mut line_ids
};
let id = quoted_circuit_id(preferred, (branch.from, branch.to), used, sanitized);
rows.entry((branch.from, branch.to, transformers, id.trim().to_owned()))
.or_default()
.push(row);
ids[row] = id;
}
}
(ids, rows)
}
fn switch_index(net: &BalancedNetwork, sanitized: &mut usize) -> (Vec<String>, SwitchRows) {
let mut ids = Vec::with_capacity(net.switches().len());
let mut rows: SwitchRows = BTreeMap::new();
let mut used = BTreeMap::new();
for (row, switch) in net.switches().iter().enumerate() {
let preferred = switch
.extras
.get("psse_ckt")
.and_then(serde_json::Value::as_str);
let id = quoted_circuit_id(preferred, (switch.from, switch.to), &mut used, sanitized);
rows.entry((switch.from, switch.to, id.trim().to_owned()))
.or_default()
.push(row);
ids.push(id);
}
(ids, rows)
}
fn load_index(net: &BalancedNetwork, sanitized: &mut usize) -> DeviceRows {
let mut rows: DeviceRows = BTreeMap::new();
let mut used = BTreeMap::new();
for (row, load) in net.loads().iter().enumerate() {
let id = quoted_device_id(&load.extras, load.bus, &mut used, sanitized);
rows.entry(load.bus)
.or_default()
.push((id.trim().to_owned(), row));
}
rows
}
fn shunt_index(
net: &BalancedNetwork,
sanitized: &mut usize,
) -> (DeviceRows, BTreeMap<BusId, Vec<usize>>) {
let mut fixed: DeviceRows = BTreeMap::new();
let mut switched: BTreeMap<BusId, Vec<usize>> = BTreeMap::new();
let mut used = BTreeMap::new();
for (row, shunt) in net.shunts().iter().enumerate() {
if shunt.control.is_some() {
switched.entry(shunt.bus).or_default().push(row);
continue;
}
let id = quoted_device_id(&shunt.extras, shunt.bus, &mut used, sanitized);
fixed
.entry(shunt.bus)
.or_default()
.push((id.trim().to_owned(), row));
}
(fixed, switched)
}
fn transformer_3w_index(
net: &BalancedNetwork,
sanitized: &mut usize,
) -> (Vec<String>, Transformer3wRows) {
let mut ids = Vec::with_capacity(net.transformers_3w().len());
let mut rows: Transformer3wRows = BTreeMap::new();
let mut used = BTreeMap::new();
for (row, transformer) in net.transformers_3w().iter().enumerate() {
let id = transformer_3w_id(net, transformer, &mut used, sanitized);
let buses = sorted_triple([
transformer.windings[0].bus,
transformer.windings[1].bus,
transformer.windings[2].bus,
]);
rows.entry(buses)
.or_default()
.push((id.trim().to_owned(), row));
ids.push(id);
}
(ids, rows)
}
impl EquipmentTables {
fn new(net: &BalancedNetwork) -> Self {
let mut sanitized = 0usize;
let (machine_ids, machine_rows) = machine_index(net, &mut sanitized);
let (circuit_ids, branch_rows) = branch_index(net, &mut sanitized);
let (switch_ids, switch_rows) = switch_index(net, &mut sanitized);
let (fixed_shunt_rows, switched_shunt_rows) = shunt_index(net, &mut sanitized);
let (transformer_3w_ids, transformer_3w_rows) = transformer_3w_index(net, &mut sanitized);
Self {
machine_ids,
circuit_ids,
switch_ids,
transformer_3w_ids,
bus_rows: net
.buses()
.iter()
.enumerate()
.map(|(row, bus)| (bus.id, row))
.collect(),
branch_rows,
switch_rows,
machine_rows,
fixed_shunt_rows,
switched_shunt_rows,
load_rows: load_index(net, &mut sanitized),
transformer_3w_rows,
names: super::tara::NameIndex::new(net),
}
}
}
impl<'n> PsseEquipmentIndex<'n> {
#[must_use]
pub fn new(net: &'n BalancedNetwork) -> Self {
Self {
net,
tables: Cow::Owned(EquipmentTables::new(net)),
}
}
#[must_use]
pub fn network(&self) -> &'n BalancedNetwork {
self.net
}
#[must_use]
pub fn machine_ids(&self) -> &[String] {
&self.tables.machine_ids
}
#[must_use]
pub fn circuit_ids(&self) -> &[String] {
&self.tables.circuit_ids
}
#[must_use]
pub fn switch_ids(&self) -> &[String] {
&self.tables.switch_ids
}
#[must_use]
pub fn transformer_3w_ids(&self) -> &[String] {
&self.tables.transformer_3w_ids
}
#[must_use]
pub fn bus_row(&self, bus: BusId) -> Option<usize> {
self.tables.bus_rows.get(&bus).copied()
}
#[must_use]
pub fn branch_rows(&self, from: BusId, to: BusId, circuit: &str) -> Vec<usize> {
let lines = self.family_rows(from, to, circuit, false);
if lines.is_empty() {
self.family_rows(from, to, circuit, true)
} else {
lines
}
}
#[must_use]
pub fn switch_rows(&self, from: BusId, to: BusId, circuit: &str) -> Vec<usize> {
let circuit = circuit.trim();
let mut rows = Vec::new();
if let Some(forward) = self.tables.switch_rows.get(&(from, to, circuit.to_owned())) {
rows.extend_from_slice(forward);
}
if from != to
&& let Some(reverse) = self.tables.switch_rows.get(&(to, from, circuit.to_owned()))
{
rows.extend_from_slice(reverse);
}
rows
}
fn family_rows(&self, from: BusId, to: BusId, circuit: &str, transformer: bool) -> Vec<usize> {
let circuit = circuit.trim();
let mut rows = Vec::new();
if let Some(forward) =
self.tables
.branch_rows
.get(&(from, to, transformer, circuit.to_owned()))
{
rows.extend_from_slice(forward);
}
if from != to
&& let Some(reverse) =
self.tables
.branch_rows
.get(&(to, from, transformer, circuit.to_owned()))
{
rows.extend_from_slice(reverse);
}
rows
}
#[must_use]
pub fn machine_row(&self, bus: BusId, id: &str) -> Option<usize> {
self.tables
.machine_rows
.get(&(bus, id.trim().to_owned()))
.copied()
}
#[must_use]
pub fn fixed_shunt_rows(&self, bus: BusId, id: Option<&str>) -> Vec<usize> {
select_rows(self.tables.fixed_shunt_rows.get(&bus), id)
}
#[must_use]
pub fn switched_shunt_rows(&self, bus: BusId) -> Vec<usize> {
self.tables
.switched_shunt_rows
.get(&bus)
.cloned()
.unwrap_or_default()
}
#[must_use]
pub fn load_rows(&self, bus: BusId, id: Option<&str>) -> Vec<usize> {
select_rows(self.tables.load_rows.get(&bus), id)
}
#[must_use]
pub fn transformer_3w_row(&self, buses: [BusId; 3], circuit: &str) -> Option<usize> {
self.transformer_3w_rows(buses, circuit).first().copied()
}
#[must_use]
pub fn transformer_3w_rows(&self, buses: [BusId; 3], circuit: &str) -> Vec<usize> {
let circuit = circuit.trim();
self.tables
.transformer_3w_rows
.get(&sorted_triple(buses))
.into_iter()
.flatten()
.filter(|(id, _)| id == circuit)
.map(|(_, row)| *row)
.collect()
}
}
#[derive(Debug, Clone)]
pub struct SharedPsseEquipmentIndex {
net: Arc<BalancedNetwork>,
tables: EquipmentTables,
}
impl SharedPsseEquipmentIndex {
#[must_use]
pub fn new(net: impl Into<Arc<BalancedNetwork>>) -> Self {
let net = net.into();
let tables = EquipmentTables::new(&net);
Self { net, tables }
}
#[must_use]
pub fn network(&self) -> &Arc<BalancedNetwork> {
&self.net
}
#[must_use]
pub fn index(&self) -> PsseEquipmentIndex<'_> {
PsseEquipmentIndex {
net: &self.net,
tables: Cow::Borrowed(&self.tables),
}
}
}
fn select_rows(at_bus: Option<&Vec<(String, usize)>>, id: Option<&str>) -> Vec<usize> {
let Some(devices) = at_bus else {
return Vec::new();
};
match id {
None => devices.iter().map(|(_, row)| *row).collect(),
Some(wanted) => {
let wanted = wanted.trim();
devices
.iter()
.filter(|(id, _)| id == wanted)
.map(|(_, row)| *row)
.collect()
}
}
}
#[derive(Debug, Clone, Default, PartialEq)]
#[non_exhaustive]
pub struct ContingencyResolution {
pub cases: Vec<ResolvedCase>,
pub resolved: usize,
pub unresolved: usize,
pub unrecognized_statements: usize,
}
impl ContingencyResolution {
#[must_use]
pub fn diagnostics(&self) -> Vec<Diagnostic> {
let mut notes = Vec::new();
for case in self.cases.iter().filter(|case| !case.is_resolved()) {
if notes.len() == MAX_READER_NOTES {
notes.push(Diagnostic::of(
&codes::BUILD_CON_NOTES_TRUNCATED,
"further resolution notes suppressed",
));
break;
}
let first = &case.unresolved[0];
notes.push(Diagnostic::of(
&codes::BUILD_CON_CASE_UNRESOLVED,
format!(
"contingency '{}': {}",
case.name,
describe(&first.action, first.reason)
),
));
}
notes
}
}
#[derive(Debug, Clone, Default, PartialEq)]
#[non_exhaustive]
pub struct ResolvedCase {
pub name: String,
pub components: Vec<ResolvedComponent>,
pub unresolved: Vec<UnresolvedAction>,
}
impl ResolvedCase {
#[must_use]
pub fn is_resolved(&self) -> bool {
self.unresolved.is_empty()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct ResolvedComponent {
pub component_type: &'static str,
pub id: Option<ComponentId>,
pub row: usize,
pub in_service: bool,
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct UnresolvedAction {
pub action: ContingencyAction,
pub reason: UnresolvedReason,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum UnresolvedReason {
NoSuchBus,
NoSuchBranch,
AmbiguousBranch {
matches: usize,
},
AmbiguousTransformer3w {
matches: usize,
},
NoSuchMachine,
NoSuchShunt,
NoSuchLoad,
NoSuchTransformer3w,
Unrecognized,
NoSuchBusName,
AmbiguousBusName {
matches: usize,
},
NoSuchBranchName,
AmbiguousBranchName {
matches: usize,
},
}
impl UnresolvedReason {
#[must_use]
pub fn name(&self) -> &'static str {
match self {
Self::NoSuchBus => "no_such_bus",
Self::NoSuchBranch => "no_such_branch",
Self::AmbiguousBranch { .. } => "ambiguous_branch",
Self::AmbiguousTransformer3w { .. } => "ambiguous_transformer_3w",
Self::NoSuchMachine => "no_such_machine",
Self::NoSuchShunt => "no_such_shunt",
Self::NoSuchLoad => "no_such_load",
Self::NoSuchTransformer3w => "no_such_transformer_3w",
Self::Unrecognized => "unrecognized",
Self::NoSuchBusName => "no_such_bus_name",
Self::AmbiguousBusName { .. } => "ambiguous_bus_name",
Self::NoSuchBranchName => "no_such_branch_name",
Self::AmbiguousBranchName { .. } => "ambiguous_branch_name",
}
}
}
fn describe(action: &ContingencyAction, reason: UnresolvedReason) -> String {
match (action, reason) {
(ContingencyAction::OpenBranch { from, to, circuit }, UnresolvedReason::NoSuchBranch) => {
format!("no branch {from} to {to} circuit {circuit}")
}
(
ContingencyAction::OpenBranch { from, to, circuit },
UnresolvedReason::AmbiguousBranch { matches },
) => format!("branch {from} to {to} circuit {circuit} names {matches} branches"),
(
ContingencyAction::OpenThreeWinding { buses, circuit },
UnresolvedReason::AmbiguousTransformer3w { matches },
) => format!(
"three winding transformer on buses {} {} {} circuit {circuit} names {matches} transformers",
buses[0], buses[1], buses[2]
),
(ContingencyAction::OpenThreeWinding { buses, circuit }, _) => format!(
"no three winding transformer on buses {} {} {} circuit {circuit}",
buses[0], buses[1], buses[2]
),
(
ContingencyAction::RemoveMachine { bus, id }
| ContingencyAction::AddMachine { bus, id },
_,
) => format!("no machine {id} at bus {bus}"),
(ContingencyAction::RemoveShunt { bus, id }, _) => match id {
Some(id) => format!("no fixed shunt {id} at bus {bus}"),
None => format!("no fixed shunt at bus {bus}"),
},
(ContingencyAction::RemoveSwitchedShunt { bus }, _) => {
format!("no switched shunt at bus {bus}")
}
(ContingencyAction::RemoveLoad { bus, id }, _) => match id {
Some(id) => format!("no load {id} at bus {bus}"),
None => format!("no load at bus {bus}"),
},
(ContingencyAction::Unrecognized { text }, UnresolvedReason::NoSuchBusName) => {
format!("a bus name names no bus: {text}")
}
(
ContingencyAction::Unrecognized { text },
UnresolvedReason::AmbiguousBusName { matches },
) => {
format!("a bus name names {matches} buses: {text}")
}
(ContingencyAction::Unrecognized { text }, UnresolvedReason::NoSuchBranchName) => {
format!("the branch name names no branch: {text}")
}
(
ContingencyAction::Unrecognized { text },
UnresolvedReason::AmbiguousBranchName { matches },
) => format!("the branch name names {matches} branches: {text}"),
(ContingencyAction::Unrecognized { text }, _) => {
format!("statement kept as text: {text}")
}
(
ContingencyAction::DisconnectBus { bus }
| ContingencyAction::ChangeLoad { bus, .. }
| ContingencyAction::ChangeGeneration { bus, .. },
_,
) => format!("no bus {bus}"),
(ContingencyAction::OpenBranch { from, to, circuit }, _) => {
format!("branch {from} to {to} circuit {circuit} did not bind")
}
}
}
impl ContingencySet {
#[must_use]
pub fn resolve(&self, net: &BalancedNetwork) -> ContingencyResolution {
self.resolve_with(&PsseEquipmentIndex::new(net))
}
#[must_use]
pub fn resolve_with(&self, index: &PsseEquipmentIndex<'_>) -> ContingencyResolution {
let mut out = ContingencyResolution::default();
for case in &self.cases {
let mut resolved = ResolvedCase {
name: case.name.clone(),
..ResolvedCase::default()
};
for action in &case.actions {
if matches!(action, ContingencyAction::Unrecognized { .. }) {
out.unrecognized_statements += 1;
}
match bind(index, action) {
Ok(components) => resolved.components.extend(components),
Err(reason) => resolved.unresolved.push(UnresolvedAction {
action: action.clone(),
reason,
}),
}
}
if resolved.is_resolved() {
out.resolved += 1;
} else {
out.unresolved += 1;
}
out.cases.push(resolved);
}
out
}
}
fn bind(
index: &PsseEquipmentIndex<'_>,
action: &ContingencyAction,
) -> Result<Vec<ResolvedComponent>, UnresolvedReason> {
let net = index.network();
match action {
ContingencyAction::OpenBranch { from, to, circuit } => {
let rows = index.branch_rows(*from, *to, circuit);
let (rows, component): (_, fn(&BalancedNetwork, usize) -> ResolvedComponent) =
if rows.is_empty() {
(index.switch_rows(*from, *to, circuit), switch_component)
} else {
(rows, branch_component)
};
match rows.as_slice() {
[] => Err(UnresolvedReason::NoSuchBranch),
[row] => Ok(vec![component(net, *row)]),
many => Err(UnresolvedReason::AmbiguousBranch {
matches: many.len(),
}),
}
}
ContingencyAction::OpenThreeWinding { buses, circuit } => {
let rows = index.transformer_3w_rows(*buses, circuit);
match rows.as_slice() {
[] => Err(UnresolvedReason::NoSuchTransformer3w),
[row] => Ok(vec![transformer_3w_component(net, *row)]),
many => Err(UnresolvedReason::AmbiguousTransformer3w {
matches: many.len(),
}),
}
}
ContingencyAction::RemoveMachine { bus, id }
| ContingencyAction::AddMachine { bus, id } => index
.machine_row(*bus, id)
.map(|row| vec![generator_component(net, row)])
.ok_or(UnresolvedReason::NoSuchMachine),
ContingencyAction::RemoveShunt { bus, id } => {
let rows = index.fixed_shunt_rows(*bus, id.as_deref());
non_empty(rows, UnresolvedReason::NoSuchShunt).map(|rows| {
rows.into_iter()
.map(|row| shunt_component(net, row))
.collect()
})
}
ContingencyAction::RemoveSwitchedShunt { bus } => {
let rows = index.switched_shunt_rows(*bus);
non_empty(rows, UnresolvedReason::NoSuchShunt).map(|rows| {
rows.into_iter()
.map(|row| shunt_component(net, row))
.collect()
})
}
ContingencyAction::RemoveLoad { bus, id } => {
let rows = index.load_rows(*bus, id.as_deref());
non_empty(rows, UnresolvedReason::NoSuchLoad).map(|rows| {
rows.into_iter()
.map(|row| load_component(net, row))
.collect()
})
}
ContingencyAction::DisconnectBus { bus }
| ContingencyAction::ChangeLoad { bus, .. }
| ContingencyAction::ChangeGeneration { bus, .. } => index
.bus_row(*bus)
.map(|row| vec![bus_component(net, row)])
.ok_or(UnresolvedReason::NoSuchBus),
ContingencyAction::Unrecognized { text } => {
match super::tara::read_text_action(&index.tables.names, text) {
None => Err(UnresolvedReason::Unrecognized),
Some(Err(reason)) => Err(reason),
Some(Ok(super::tara::TextAction::Action(action))) => bind(index, &action),
Some(Ok(super::tara::TextAction::Branch(row))) => {
Ok(vec![branch_component(net, row)])
}
}
}
}
}
fn non_empty(rows: Vec<usize>, reason: UnresolvedReason) -> Result<Vec<usize>, UnresolvedReason> {
if rows.is_empty() {
Err(reason)
} else {
Ok(rows)
}
}
fn component_id(component_type: &str, uid: Option<&str>) -> Option<ComponentId> {
let local = uid.filter(|uid| !uid.is_empty())?;
ComponentId::new(component_type, local).ok()
}
fn bus_component(net: &BalancedNetwork, row: usize) -> ResolvedComponent {
let bus = &net.buses()[row];
ResolvedComponent {
component_type: BUS,
id: component_id(BUS, bus.uid.as_deref()),
row,
in_service: bus.kind != BusType::Isolated,
}
}
fn load_component(net: &BalancedNetwork, row: usize) -> ResolvedComponent {
let load = &net.loads()[row];
ResolvedComponent {
component_type: LOAD,
id: component_id(LOAD, load.uid.as_deref()),
row,
in_service: load.in_service,
}
}
fn shunt_component(net: &BalancedNetwork, row: usize) -> ResolvedComponent {
let shunt = &net.shunts()[row];
ResolvedComponent {
component_type: SHUNT,
id: component_id(SHUNT, shunt.uid.as_deref()),
row,
in_service: shunt.in_service,
}
}
fn generator_component(net: &BalancedNetwork, row: usize) -> ResolvedComponent {
let generator = &net.generators()[row];
ResolvedComponent {
component_type: GENERATOR,
id: component_id(GENERATOR, generator.uid.as_deref()),
row,
in_service: generator.in_service,
}
}
fn branch_component(net: &BalancedNetwork, row: usize) -> ResolvedComponent {
let branch = &net.branches()[row];
ResolvedComponent {
component_type: BRANCH,
id: component_id(BRANCH, branch.uid.as_deref()),
row,
in_service: branch.in_service,
}
}
fn switch_component(net: &BalancedNetwork, row: usize) -> ResolvedComponent {
let switch = &net.switches()[row];
ResolvedComponent {
component_type: SWITCH,
id: component_id(SWITCH, switch.uid.as_deref()),
row,
in_service: switch.closed,
}
}
fn transformer_3w_component(net: &BalancedNetwork, row: usize) -> ResolvedComponent {
let transformer = &net.transformers_3w()[row];
ResolvedComponent {
component_type: TRANSFORMER_3W,
id: component_id(TRANSFORMER_3W, transformer.uid.as_deref()),
row,
in_service: transformer.in_service,
}
}