use std::collections::BTreeSet;
use super::expand::low_voltage_bus;
use super::lexer::{LexedLine, LineKind, lex};
use super::sub::{SubsystemSet, decimal};
use super::{PsseEquipmentIndex, RetainedStatement, field, note_within_budget};
use crate::diagnostics::{Diagnostic, codes};
use crate::network::{BalancedNetwork, BusId};
use crate::{Error, Result};
const FMT: &str = "psse monitored elements";
const KV_TOLERANCE: f64 = 1e-6;
#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct MonitoredSet {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub header: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub statements: Vec<MonitorStatement>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub retained: Vec<RetainedStatement>,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(tag = "kind", rename_all = "snake_case")]
#[non_exhaustive]
pub enum MonitorStatement {
BranchesInSubsystem {
subsystem: String,
low_voltage_3w: bool,
},
TiesFromSubsystem { subsystem: String },
Branches {
branches: Vec<BranchRef>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
retained: Vec<RetainedStatement>,
},
Interface {
name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
rating_mw: Option<f64>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
branches: Vec<BranchRef>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
retained: Vec<RetainedStatement>,
},
VoltageRange {
scope: MonitorScope,
vmin: f64,
vmax: f64,
},
VoltageDeviation {
scope: MonitorScope,
down: f64,
#[serde(default, skip_serializing_if = "Option::is_none")]
up: Option<f64>,
},
BranchesInSubsystemKvRange {
subsystem: String,
low_voltage_3w: bool,
kv_range: (f64, f64),
},
TiesFromSubsystemKvRange {
subsystem: String,
kv_range: (f64, f64),
},
VoltageRangeWithPre {
scope: MonitorScope,
vmin: f64,
vmax: f64,
pre_min: f64,
pre_max: f64,
},
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct BranchRef {
pub from: BusId,
pub to: BusId,
pub circuit: String,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(tag = "kind", rename_all = "snake_case")]
#[non_exhaustive]
pub enum MonitorScope {
AllBuses,
Subsystem {
name: String,
},
Bus {
bus: BusId,
},
Area {
area: usize,
},
Zone {
zone: usize,
},
Owner {
owner: usize,
},
Kv {
kv: f64,
},
SubsystemKv {
name: String,
kv: f64,
},
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct MonitoredParsed {
pub set: MonitoredSet,
pub diagnostics: Vec<Diagnostic>,
}
impl MonitoredParsed {
fn note(&mut self, info: &'static crate::diagnostics::DiagnosticInfo, message: String) {
note_within_budget(
&mut self.diagnostics,
info,
&codes::READ_MON_NOTES_TRUNCATED,
message,
);
}
fn unrecognized(&mut self, number: usize, text: &str) {
self.note(
&codes::READ_MON_STATEMENT_UNRECOGNIZED,
format!("line {number}: statement kept as text: {text}"),
);
}
}
fn bad(message: String) -> Error {
Error::FormatRead {
format: FMT,
message,
}
}
impl MonitoredSet {
pub fn parse(text: &str) -> Result<MonitoredParsed> {
let mut reader = Reader::new();
for line in lex(text) {
reader.read_line(&line);
}
reader.finish()
}
#[must_use]
pub fn to_mon(&self) -> String {
let mut out = String::new();
for line in &self.header {
out.push_str(line);
out.push('\n');
}
for statement in &self.statements {
out.push_str(&write_statement(statement));
}
for statement in self.retained.iter().filter(|kept| !kept.after_end) {
out.push_str(&statement.text);
out.push('\n');
}
out.push_str("END\n");
for statement in self.retained.iter().filter(|kept| kept.after_end) {
out.push_str(&statement.text);
out.push('\n');
}
out
}
}
struct OpenBlock {
opened: usize,
interface: Option<(String, Option<f64>)>,
branches: Vec<BranchRef>,
retained: Vec<RetainedStatement>,
}
struct Reader {
parsed: MonitoredParsed,
block: Option<OpenBlock>,
seen_statement: bool,
ended: bool,
noted_text_after_end: bool,
band_order_noted_at: Option<usize>,
}
impl Reader {
fn new() -> Self {
Reader {
parsed: MonitoredParsed {
set: MonitoredSet::default(),
diagnostics: Vec::new(),
},
block: None,
seen_statement: false,
ended: false,
noted_text_after_end: false,
band_order_noted_at: None,
}
}
fn read_line(&mut self, line: &LexedLine<'_>) {
if self.block.is_some() {
self.read_block_line(line);
return;
}
if self.ended {
self.keep_after_end(line);
return;
}
if !self.take_header(line) {
return;
}
let upper = line.keywords();
let words = line.words();
self.read_statement(line, &upper, &words);
}
fn finish(self) -> Result<MonitoredParsed> {
if let Some(open) = self.block {
let what = match open.interface {
Some((name, _)) => format!("MONITOR INTERFACE '{name}'"),
None => "MONITOR BRANCHES".to_owned(),
};
return Err(bad(format!("line {}: {what} has no END", open.opened)));
}
Ok(self.parsed)
}
fn take_header(&mut self, line: &LexedLine<'_>) -> bool {
if self.seen_statement {
return line.kind == LineKind::Statement;
}
match line.kind {
LineKind::Blank => false,
LineKind::Comment => {
self.parsed.set.header.push(line.text.to_owned());
false
}
LineKind::Statement => {
self.seen_statement = true;
true
}
}
}
fn keep_after_end(&mut self, line: &LexedLine<'_>) {
if line.kind != LineKind::Statement || is_end(line) {
return;
}
if !self.noted_text_after_end {
self.noted_text_after_end = true;
self.parsed.note(
&codes::READ_MON_TEXT_AFTER_END,
format!("line {}: text follows the file END", line.number),
);
}
self.keep_statement(line, true);
}
fn keep_statement(&mut self, line: &LexedLine<'_>, after_end: bool) {
self.parsed.set.retained.push(RetainedStatement {
line: line.number,
text: line.trimmed().to_owned(),
after_end,
});
}
fn read_block_line(&mut self, line: &LexedLine<'_>) {
if line.kind != LineKind::Statement {
return;
}
if is_end(line) {
let Some(open) = self.block.take() else {
return;
};
self.parsed.set.statements.push(match open.interface {
Some((name, rating_mw)) => MonitorStatement::Interface {
name,
rating_mw,
branches: open.branches,
retained: open.retained,
},
None => MonitorStatement::Branches {
branches: open.branches,
retained: open.retained,
},
});
return;
}
let words = line.words();
if let Some(branch) = parse_branch_ref(&words) {
if let Some(open) = self.block.as_mut() {
open.branches.push(branch);
}
return;
}
self.parsed.note(
&codes::READ_MON_SOURCE_MALFORMED,
format!(
"line {}: a monitored block line states no branch and was kept as text: {}",
line.number,
line.trimmed()
),
);
if let Some(open) = self.block.as_mut() {
open.retained.push(RetainedStatement {
line: line.number,
text: line.trimmed().to_owned(),
after_end: false,
});
}
}
fn read_statement(&mut self, line: &LexedLine<'_>, upper: &[String], words: &[&str]) {
if is_end(line) {
self.ended = true;
return;
}
if upper[0] == "MONITOR"
&& let Some(read) = self.read_monitor(line, upper, words)
{
match read {
Read::Statement(statement) => {
if matches!(statement, MonitorStatement::VoltageRangeWithPre { .. }) {
self.note_band_order(line.number);
}
self.parsed.set.statements.push(statement);
}
Read::OpenedBlock => {}
}
return;
}
self.parsed.unrecognized(line.number, line.trimmed());
self.keep_statement(line, false);
}
fn note_band_order(&mut self, number: usize) {
if self.band_order_noted_at.is_some() {
return;
}
self.band_order_noted_at = Some(number);
self.parsed.note(
&codes::READ_MON_VOLTAGE_BAND_ORDER_ASSUMED,
format!(
"line {number}: MONITOR VOLTAGE RANGE states four values; every four-value range \
in this file is read as the post-contingency low and high, then the \
pre-contingency low and high, an order no public PSS/E or TARA source states"
),
);
}
fn read_monitor(
&mut self,
line: &LexedLine<'_>,
upper: &[String],
words: &[&str],
) -> Option<Read> {
match upper.get(1)?.as_str() {
"BRANCHES" | "LINES" => {
if upper.len() == 2 {
self.block = Some(OpenBlock {
opened: line.number,
interface: None,
branches: Vec::new(),
retained: Vec::new(),
});
return Some(Read::OpenedBlock);
}
let (subsystem, low_voltage_3w, kv_range) = parse_in_subsystem_kv(upper, words, 2)?;
Some(Read::Statement(match kv_range {
Some(kv_range) => MonitorStatement::BranchesInSubsystemKvRange {
subsystem,
low_voltage_3w,
kv_range,
},
None => MonitorStatement::BranchesInSubsystem {
subsystem,
low_voltage_3w,
},
}))
}
"TIES" => {
let (subsystem, low_voltage_3w, kv_range) = parse_in_subsystem_kv(upper, words, 2)?;
(!low_voltage_3w).then_some(Read::Statement(match kv_range {
Some(kv_range) => MonitorStatement::TiesFromSubsystemKvRange {
subsystem,
kv_range,
},
None => MonitorStatement::TiesFromSubsystem { subsystem },
}))
}
"INTERFACE" => {
let name = words.get(2)?.trim().to_owned();
let rating_mw = match parse_rating(upper, 3) {
Rating::Absent => None,
Rating::Stated(value) => Some(value),
Rating::Unreadable => return None,
};
self.block = Some(OpenBlock {
opened: line.number,
interface: Some((name, rating_mw)),
branches: Vec::new(),
retained: Vec::new(),
});
Some(Read::OpenedBlock)
}
"VOLTAGE" => parse_voltage(upper, words).map(Read::Statement),
"BRANCH" | "LINE" => parse_branch_ref(&words[1..]).map(|branch| {
Read::Statement(MonitorStatement::Branches {
branches: vec![branch],
retained: Vec::new(),
})
}),
_ => None,
}
}
}
enum Read {
Statement(MonitorStatement),
OpenedBlock,
}
fn is_end(line: &LexedLine<'_>) -> bool {
line.kind == LineKind::Statement
&& line.tokens.len() == 1
&& line.tokens[0].text.eq_ignore_ascii_case("END")
}
type SubsystemMonitorSpec = (String, bool, Option<(f64, f64)>);
fn parse_in_subsystem_kv(
upper: &[String],
words: &[&str],
at: usize,
) -> Option<SubsystemMonitorSpec> {
let subsystem = words.get(at + 2)?.trim().to_owned();
if !matches!(upper.get(at)?.as_str(), "IN" | "FROM") || upper.get(at + 1)? != "SUBSYSTEM" {
return None;
}
let mut next = at + 3;
let low_voltage_3w = upper.get(next).is_some_and(|word| word == "3WLOWVOLTAGE");
if low_voltage_3w {
next += 1;
}
if upper.get(next).is_some_and(|word| word == "IN")
&& upper.get(next + 1).is_some_and(|word| word == "KVRANGE")
{
next += 1;
}
let kv_range = if upper.get(next).is_some_and(|word| word == "KVRANGE") {
let lo = upper.get(next + 1)?.parse::<f64>().ok()?;
let hi = upper.get(next + 2)?.parse::<f64>().ok()?;
if !lo.is_finite() || !hi.is_finite() || lo > hi {
return None;
}
next += 3;
Some((lo, hi))
} else {
None
};
(next == upper.len()).then_some((subsystem, low_voltage_3w, kv_range))
}
enum Rating {
Absent,
Stated(f64),
Unreadable,
}
fn parse_rating(upper: &[String], at: usize) -> Rating {
if at >= upper.len() {
return Rating::Absent;
}
if upper[at] != "RATING" {
return Rating::Unreadable;
}
let Some(value) = upper
.get(at + 1)
.and_then(|word| word.parse::<f64>().ok())
.filter(|value| value.is_finite())
else {
return Rating::Unreadable;
};
let mut next = at + 2;
if upper.get(next).is_some_and(|word| word == "MW") {
next += 1;
}
if next == upper.len() {
Rating::Stated(value)
} else {
Rating::Unreadable
}
}
fn parse_voltage(upper: &[String], words: &[&str]) -> Option<MonitorStatement> {
let deviation = match upper.get(2)?.as_str() {
"RANGE" => false,
"DEVIATION" => true,
_ => return None,
};
let (scope, at) = parse_scope(upper, words, 3)?;
let values: Option<Vec<f64>> = upper[at..]
.iter()
.map(|word| word.parse::<f64>().ok().filter(|value| value.is_finite()))
.collect();
let values = values?;
match (deviation, values.as_slice()) {
(false, [vmin, vmax]) => (vmin <= vmax).then_some(MonitorStatement::VoltageRange {
scope,
vmin: *vmin,
vmax: *vmax,
}),
(false, [vmin, vmax, pre_min, pre_max]) => {
(vmin <= vmax && pre_min <= pre_max).then_some(MonitorStatement::VoltageRangeWithPre {
scope,
vmin: *vmin,
vmax: *vmax,
pre_min: *pre_min,
pre_max: *pre_max,
})
}
(true, [down]) => Some(MonitorStatement::VoltageDeviation {
scope,
down: *down,
up: None,
}),
(true, [down, up]) => Some(MonitorStatement::VoltageDeviation {
scope,
down: *down,
up: Some(*up),
}),
_ => None,
}
}
fn parse_scope(upper: &[String], words: &[&str], at: usize) -> Option<(MonitorScope, usize)> {
let integer = |offset: usize| upper.get(at + offset)?.parse::<usize>().ok();
match upper.get(at)?.as_str() {
"ALL" if upper.get(at + 1).is_some_and(|word| word == "BUSES") => {
Some((MonitorScope::AllBuses, at + 2))
}
"SUBSYSTEM" => {
let name = words.get(at + 1)?.trim().to_owned();
let mut next = at + 2;
let scope = if upper.get(next).is_some_and(|word| word == "KV") {
let value = upper.get(next + 1)?.parse::<f64>().ok()?;
if !value.is_finite() {
return None;
}
next += 2;
MonitorScope::SubsystemKv { name, kv: value }
} else {
MonitorScope::Subsystem { name }
};
Some((scope, next))
}
"BUS" => Some((
MonitorScope::Bus {
bus: BusId(integer(1)?),
},
at + 2,
)),
"AREA" => Some((MonitorScope::Area { area: integer(1)? }, at + 2)),
"ZONE" => Some((MonitorScope::Zone { zone: integer(1)? }, at + 2)),
"OWNER" => Some((MonitorScope::Owner { owner: integer(1)? }, at + 2)),
"KV" => {
let kv = upper.get(at + 1)?.parse::<f64>().ok()?;
kv.is_finite().then_some((MonitorScope::Kv { kv }, at + 2))
}
_ => None,
}
}
fn parse_branch_ref(words: &[&str]) -> Option<BranchRef> {
let words = match words {
[monitor, noun, ..]
if monitor.eq_ignore_ascii_case("MONITOR")
&& (noun.eq_ignore_ascii_case("BRANCH") || noun.eq_ignore_ascii_case("LINE")) =>
{
&words[1..]
}
_ => words,
};
if words.first()?.parse::<usize>().is_err() {
let upper: Vec<String> = words.iter().map(|word| word.to_ascii_uppercase()).collect();
let mut at = usize::from(matches!(upper[0].as_str(), "BRANCH" | "LINE"));
super::take_keyword(&upper, &mut at, "FROM")?;
let from = super::take_bus(&upper, &mut at)?;
super::take_keyword(&upper, &mut at, "TO")?;
let to = super::take_bus(&upper, &mut at)?;
let circuit = super::take_circuit(&upper, words, &mut at)?;
return Some(BranchRef { from, to, circuit });
}
if words.len() > 3 {
return None;
}
let from = words.first()?.parse::<usize>().ok()?;
let to = words.get(1)?.parse::<usize>().ok()?;
let circuit = words.get(2).map_or("1", |word| word.trim());
Some(BranchRef {
from: BusId(from),
to: BusId(to),
circuit: if circuit.is_empty() {
"1".to_owned()
} else {
circuit.to_owned()
},
})
}
fn write_scope(scope: &MonitorScope) -> String {
match scope {
MonitorScope::AllBuses => "ALL BUSES".to_owned(),
MonitorScope::Subsystem { name } => format!("SUBSYSTEM '{name}'"),
MonitorScope::SubsystemKv { name, kv } => format!("SUBSYSTEM '{name}' KV {}", decimal(*kv)),
MonitorScope::Bus { bus } => format!("BUS {}", bus.0),
MonitorScope::Area { area } => format!("AREA {area}"),
MonitorScope::Zone { zone } => format!("ZONE {zone}"),
MonitorScope::Owner { owner } => format!("OWNER {owner}"),
MonitorScope::Kv { kv } => format!("KV {}", decimal(*kv)),
}
}
fn write_branch_block(
head: &str,
branches: &[BranchRef],
retained: &[RetainedStatement],
) -> String {
use std::fmt::Write as _;
let mut out = format!("{head}\n");
for branch in branches {
let _ = writeln!(
out,
"{:>6} {:>6} {}",
branch.from.0,
branch.to.0,
field(&branch.circuit)
);
}
for statement in retained {
out.push_str(&statement.text);
out.push('\n');
}
out.push_str("END\n");
out
}
fn write_statement(statement: &MonitorStatement) -> String {
match statement {
MonitorStatement::BranchesInSubsystem {
subsystem,
low_voltage_3w,
} => {
let low = if *low_voltage_3w { " 3WLOWVOLTAGE" } else { "" };
format!("MONITOR BRANCHES IN SUBSYSTEM '{subsystem}'{low}\n")
}
MonitorStatement::BranchesInSubsystemKvRange {
subsystem,
low_voltage_3w,
kv_range: (lo, hi),
} => {
let low = if *low_voltage_3w { " 3WLOWVOLTAGE" } else { "" };
format!(
"MONITOR BRANCHES IN SUBSYSTEM '{subsystem}'{low} KVRANGE {} {}\n",
decimal(*lo),
decimal(*hi)
)
}
MonitorStatement::TiesFromSubsystem { subsystem } => {
format!("MONITOR TIES FROM SUBSYSTEM '{subsystem}'\n")
}
MonitorStatement::TiesFromSubsystemKvRange {
subsystem,
kv_range: (lo, hi),
} => {
format!(
"MONITOR TIES FROM SUBSYSTEM '{subsystem}' KVRANGE {} {}\n",
decimal(*lo),
decimal(*hi)
)
}
MonitorStatement::Branches { branches, retained } => {
write_branch_block("MONITOR BRANCHES", branches, retained)
}
MonitorStatement::Interface {
name,
rating_mw,
branches,
retained,
} => {
let head = match rating_mw {
Some(rating) => {
format!("MONITOR INTERFACE '{name}' RATING {} MW", decimal(*rating))
}
None => format!("MONITOR INTERFACE '{name}'"),
};
write_branch_block(&head, branches, retained)
}
MonitorStatement::VoltageRange { scope, vmin, vmax } => format!(
"MONITOR VOLTAGE RANGE {} {} {}\n",
write_scope(scope),
decimal(*vmin),
decimal(*vmax)
),
MonitorStatement::VoltageRangeWithPre {
scope,
vmin,
vmax,
pre_min,
pre_max,
} => format!(
"MONITOR VOLTAGE RANGE {} {} {} {} {}\n",
write_scope(scope),
decimal(*vmin),
decimal(*vmax),
decimal(*pre_min),
decimal(*pre_max)
),
MonitorStatement::VoltageDeviation { scope, down, up } => {
let tail = match up {
Some(up) => format!(" {}", decimal(*up)),
None => String::new(),
};
format!(
"MONITOR VOLTAGE DEVIATION {} {}{tail}\n",
write_scope(scope),
decimal(*down)
)
}
}
}
#[derive(Debug, Clone, Default, PartialEq)]
#[non_exhaustive]
pub struct MonitoredResolution {
pub branch_rows: BTreeSet<usize>,
pub transformer_3w_rows: BTreeSet<usize>,
pub tie_rows: BTreeSet<usize>,
pub interfaces: Vec<ResolvedInterface>,
pub voltage_ranges: Vec<ResolvedVoltageScope>,
pub voltage_ranges_with_pre: Vec<(ResolvedVoltageScope, (f64, f64))>,
pub voltage_deviations: Vec<ResolvedVoltageScope>,
pub unresolved: Vec<UnresolvedMonitor>,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct ResolvedInterface {
pub name: String,
pub rating_mw: Option<f64>,
pub members: Vec<InterfaceMember>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct InterfaceMember {
pub row: usize,
pub reversed: bool,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct ResolvedVoltageScope {
pub bus_rows: BTreeSet<usize>,
pub low: f64,
pub high: Option<f64>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct UnresolvedMonitor {
pub statement: MonitorStatement,
pub reason: UnresolvedMonitorReason,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum UnresolvedMonitorReason {
NoSuchSubsystem,
NoSuchBranch {
from: BusId,
to: BusId,
circuit: String,
},
AmbiguousBranch {
from: BusId,
to: BusId,
circuit: String,
matches: usize,
},
}
impl MonitoredResolution {
#[must_use]
pub fn diagnostics(&self) -> Vec<Diagnostic> {
self.unresolved
.iter()
.map(|entry| {
Diagnostic::of(
&codes::BUILD_MON_STATEMENT_UNRESOLVED,
describe(&entry.statement, &entry.reason),
)
})
.collect()
}
}
fn describe(statement: &MonitorStatement, reason: &UnresolvedMonitorReason) -> String {
let what = match statement {
MonitorStatement::BranchesInSubsystem { subsystem, .. }
| MonitorStatement::BranchesInSubsystemKvRange { subsystem, .. }
| MonitorStatement::TiesFromSubsystem { subsystem }
| MonitorStatement::TiesFromSubsystemKvRange { subsystem, .. } => {
format!("monitored subsystem '{subsystem}'")
}
MonitorStatement::Branches { .. } => "monitored branches".to_owned(),
MonitorStatement::Interface { name, .. } => format!("monitored interface '{name}'"),
MonitorStatement::VoltageRange { .. } | MonitorStatement::VoltageRangeWithPre { .. } => {
"monitored voltage range".to_owned()
}
MonitorStatement::VoltageDeviation { .. } => "monitored voltage deviation".to_owned(),
};
match reason {
UnresolvedMonitorReason::NoSuchSubsystem => {
format!("{what}: the subsystem set states no such subsystem")
}
UnresolvedMonitorReason::NoSuchBranch { from, to, circuit } => {
format!("{what}: no branch {from} to {to} circuit {circuit}")
}
UnresolvedMonitorReason::AmbiguousBranch {
from,
to,
circuit,
matches,
} => format!("{what}: branch {from} to {to} circuit {circuit} names {matches} branches"),
}
}
impl MonitoredSet {
#[must_use]
pub fn resolve(&self, net: &BalancedNetwork, subsystems: &SubsystemSet) -> MonitoredResolution {
self.resolve_with(&PsseEquipmentIndex::new(net), subsystems)
}
#[must_use]
pub fn resolve_with(
&self,
index: &PsseEquipmentIndex<'_>,
subsystems: &SubsystemSet,
) -> MonitoredResolution {
let mut out = MonitoredResolution::default();
for statement in &self.statements {
resolve_statement(statement, index.network(), subsystems, index, &mut out);
}
out
}
}
#[allow(clippy::too_many_lines)]
fn resolve_statement(
statement: &MonitorStatement,
net: &BalancedNetwork,
subsystems: &SubsystemSet,
index: &PsseEquipmentIndex,
out: &mut MonitoredResolution,
) {
match statement {
MonitorStatement::BranchesInSubsystem {
subsystem,
low_voltage_3w,
} => resolve_branches_in_subsystem(
statement,
subsystem,
*low_voltage_3w,
None,
net,
subsystems,
index,
out,
),
MonitorStatement::BranchesInSubsystemKvRange {
subsystem,
low_voltage_3w,
kv_range,
} => resolve_branches_in_subsystem(
statement,
subsystem,
*low_voltage_3w,
Some(*kv_range),
net,
subsystems,
index,
out,
),
MonitorStatement::TiesFromSubsystem { subsystem } => {
resolve_ties_from_subsystem(statement, subsystem, None, net, subsystems, index, out);
}
MonitorStatement::TiesFromSubsystemKvRange {
subsystem,
kv_range,
} => {
resolve_ties_from_subsystem(
statement,
subsystem,
Some(*kv_range),
net,
subsystems,
index,
out,
);
}
MonitorStatement::Branches { branches, .. } => {
for branch in branches {
match bind_branch(index, branch) {
Ok(row) => {
out.branch_rows.insert(row);
}
Err(reason) => out.unresolved.push(UnresolvedMonitor {
statement: statement.clone(),
reason,
}),
}
}
}
MonitorStatement::Interface {
name,
rating_mw,
branches,
..
} => {
let mut resolved = ResolvedInterface {
name: name.clone(),
rating_mw: *rating_mw,
members: Vec::new(),
};
for branch in branches {
match bind_branch(index, branch) {
Ok(row) => resolved.members.push(member(index, branch, row)),
Err(reason) => out.unresolved.push(UnresolvedMonitor {
statement: statement.clone(),
reason,
}),
}
}
out.interfaces.push(resolved);
}
MonitorStatement::VoltageRange { scope, vmin, vmax } => {
let Some(bus_rows) = scope_rows(scope, net, subsystems, index) else {
unresolved_subsystem(statement, out);
return;
};
out.voltage_ranges.push(ResolvedVoltageScope {
bus_rows,
low: *vmin,
high: Some(*vmax),
});
}
MonitorStatement::VoltageRangeWithPre {
scope,
vmin,
vmax,
pre_min,
pre_max,
} => {
let Some(bus_rows) = scope_rows(scope, net, subsystems, index) else {
unresolved_subsystem(statement, out);
return;
};
out.voltage_ranges_with_pre.push((
ResolvedVoltageScope {
bus_rows,
low: *vmin,
high: Some(*vmax),
},
(*pre_min, *pre_max),
));
}
MonitorStatement::VoltageDeviation { scope, down, up } => {
let Some(bus_rows) = scope_rows(scope, net, subsystems, index) else {
unresolved_subsystem(statement, out);
return;
};
out.voltage_deviations.push(ResolvedVoltageScope {
bus_rows,
low: *down,
high: *up,
});
}
}
}
#[allow(clippy::too_many_arguments)]
fn resolve_branches_in_subsystem(
statement: &MonitorStatement,
subsystem: &str,
low_voltage_3w: bool,
kv_range: Option<(f64, f64)>,
net: &BalancedNetwork,
subsystems: &SubsystemSet,
index: &PsseEquipmentIndex,
out: &mut MonitoredResolution,
) {
let Some(buses) = select(subsystems, subsystem, net) else {
unresolved_subsystem(statement, out);
return;
};
for (row, branch) in net.branches().iter().enumerate() {
if buses.contains(&branch.from)
&& buses.contains(&branch.to)
&& kv_range.is_none_or(|(lo, hi)| {
bus_in_kv_range(index, branch.from, lo, hi)
&& bus_in_kv_range(index, branch.to, lo, hi)
})
{
out.branch_rows.insert(row);
}
}
if low_voltage_3w {
for (row, transformer) in net.transformers_3w().iter().enumerate() {
let low_bus = low_voltage_bus(net, index, transformer);
if buses.contains(&low_bus)
&& kv_range.is_none_or(|(lo, hi)| bus_in_kv_range(index, low_bus, lo, hi))
{
out.transformer_3w_rows.insert(row);
}
}
}
}
fn resolve_ties_from_subsystem(
statement: &MonitorStatement,
subsystem: &str,
kv_range: Option<(f64, f64)>,
net: &BalancedNetwork,
subsystems: &SubsystemSet,
index: &PsseEquipmentIndex,
out: &mut MonitoredResolution,
) {
let Some(buses) = select(subsystems, subsystem, net) else {
unresolved_subsystem(statement, out);
return;
};
for (row, branch) in net.branches().iter().enumerate() {
let from_inside = buses.contains(&branch.from);
let to_inside = buses.contains(&branch.to);
let in_scope = match (from_inside, to_inside, kv_range) {
(true, false, Some((lo, hi))) => bus_in_kv_range(index, branch.from, lo, hi),
(false, true, Some((lo, hi))) => bus_in_kv_range(index, branch.to, lo, hi),
(true, false, None) | (false, true, None) => true,
_ => false,
};
if from_inside != to_inside && in_scope {
out.tie_rows.insert(row);
}
}
}
fn unresolved_subsystem(statement: &MonitorStatement, out: &mut MonitoredResolution) {
out.unresolved.push(UnresolvedMonitor {
statement: statement.clone(),
reason: UnresolvedMonitorReason::NoSuchSubsystem,
});
}
fn select(subsystems: &SubsystemSet, name: &str, net: &BalancedNetwork) -> Option<BTreeSet<BusId>> {
Some(subsystems.get(name)?.select_buses(net))
}
fn bus_in_kv_range(index: &PsseEquipmentIndex<'_>, bus: BusId, lo: f64, hi: f64) -> bool {
index.bus_row(bus).is_some_and(|row| {
let kv = index.network().buses()[row].base_kv;
kv >= lo - KV_TOLERANCE && kv <= hi + KV_TOLERANCE
})
}
fn member(index: &PsseEquipmentIndex<'_>, branch: &BranchRef, row: usize) -> InterfaceMember {
InterfaceMember {
row,
reversed: index.network().branches()[row].from != branch.from,
}
}
fn bind_branch(
index: &PsseEquipmentIndex,
branch: &BranchRef,
) -> std::result::Result<usize, UnresolvedMonitorReason> {
let rows = index.branch_rows(branch.from, branch.to, &branch.circuit);
match rows.as_slice() {
[] => Err(UnresolvedMonitorReason::NoSuchBranch {
from: branch.from,
to: branch.to,
circuit: branch.circuit.clone(),
}),
[row] => Ok(*row),
many => Err(UnresolvedMonitorReason::AmbiguousBranch {
from: branch.from,
to: branch.to,
circuit: branch.circuit.clone(),
matches: many.len(),
}),
}
}
fn scope_rows(
scope: &MonitorScope,
net: &BalancedNetwork,
subsystems: &SubsystemSet,
index: &PsseEquipmentIndex,
) -> Option<BTreeSet<usize>> {
let rows_of = |buses: &BTreeSet<BusId>| -> BTreeSet<usize> {
buses.iter().filter_map(|bus| index.bus_row(*bus)).collect()
};
let matching = |keep: &dyn Fn(&crate::network::Bus) -> bool| -> BTreeSet<usize> {
net.buses()
.iter()
.enumerate()
.filter(|(_, bus)| keep(bus))
.map(|(row, _)| row)
.collect()
};
Some(match scope {
MonitorScope::AllBuses => (0..net.buses().len()).collect(),
MonitorScope::Subsystem { name } => rows_of(&select(subsystems, name, net)?),
MonitorScope::SubsystemKv { name, kv } => select(subsystems, name, net)?
.into_iter()
.filter(|bus| bus_in_kv_range(index, *bus, *kv, *kv))
.filter_map(|bus| index.bus_row(bus))
.collect(),
MonitorScope::Bus { bus } => index.bus_row(*bus).into_iter().collect(),
MonitorScope::Area { area } => matching(&|bus| bus.area == *area),
MonitorScope::Zone { zone } => matching(&|bus| bus.zone == *zone),
MonitorScope::Owner { owner } => matching(&|bus| super::sub::owner_of(bus) == *owner),
MonitorScope::Kv { kv } => matching(&|bus| (bus.base_kv - *kv).abs() <= KV_TOLERANCE),
})
}