use std::collections::BTreeSet;
use super::lexer::{LexedLine, LineKind, lex};
use super::{RetainedStatement, field, note_within_budget};
use crate::diagnostics::{Diagnostic, codes};
use crate::network::{BalancedNetwork, Bus, BusId};
use crate::{Error, Result};
const FMT: &str = "psse subsystem";
#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct SubsystemSet {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub header: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub subsystems: Vec<Subsystem>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub retained: Vec<RetainedStatement>,
}
#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct Subsystem {
pub name: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub groups: Vec<SelectorGroup>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub retained: Vec<RetainedStatement>,
}
#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct SelectorGroup {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub join: Option<JoinName>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub selectors: Vec<SubsystemSelector>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub retained: Vec<RetainedStatement>,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(tag = "kind", rename_all = "snake_case")]
#[non_exhaustive]
pub enum JoinName {
Anonymous,
Named { name: 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 SubsystemSelector {
Area {
from: usize,
to: usize,
},
Zone {
from: usize,
to: usize,
},
Owner {
from: usize,
to: usize,
},
Bus {
from: BusId,
to: BusId,
},
KvRange {
lo: f64,
hi: f64,
},
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct SubsystemParsed {
pub set: SubsystemSet,
pub diagnostics: Vec<Diagnostic>,
}
impl SubsystemParsed {
fn note(&mut self, info: &'static crate::diagnostics::DiagnosticInfo, message: String) {
note_within_budget(
&mut self.diagnostics,
info,
&codes::READ_SUB_NOTES_TRUNCATED,
message,
);
}
fn unrecognized(&mut self, number: usize, text: &str) {
self.note(
&codes::READ_SUB_STATEMENT_UNRECOGNIZED,
format!("line {number}: statement kept as text: {text}"),
);
}
fn malformed(&mut self, number: usize, text: &str) {
self.note(
&codes::READ_SUB_SOURCE_MALFORMED,
format!("line {number}: a selector states no number and was kept as text: {text}"),
);
}
}
fn bad(message: String) -> Error {
Error::FormatRead {
format: FMT,
message,
}
}
impl SubsystemSet {
pub fn parse(text: &str) -> Result<SubsystemParsed> {
let mut reader = Reader::new();
for line in lex(text) {
reader.read_line(&line)?;
}
reader.finish()
}
#[must_use]
pub fn to_sub(&self) -> String {
use std::fmt::Write as _;
let mut out = String::new();
for line in &self.header {
out.push_str(line);
out.push('\n');
}
for subsystem in &self.subsystems {
let _ = writeln!(out, "SUBSYSTEM '{}'", subsystem.name);
for group in &subsystem.groups {
match &group.join {
None => {
for selector in &group.selectors {
let _ = writeln!(out, " {}", write_selector(selector));
}
write_retained(&mut out, &group.retained);
}
Some(join) => {
match join {
JoinName::Anonymous => out.push_str(" JOIN\n"),
JoinName::Named { name } => {
let _ = writeln!(out, " JOIN '{name}'");
}
}
for selector in &group.selectors {
let _ = writeln!(out, " {}", write_selector(selector));
}
write_retained(&mut out, &group.retained);
out.push_str(" END\n");
}
}
}
write_retained(&mut out, &subsystem.retained);
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.push_str("END\n");
for statement in self.retained.iter().filter(|kept| kept.after_end) {
out.push_str(&statement.text);
out.push('\n');
}
out
}
#[must_use]
pub fn get(&self, name: &str) -> Option<&Subsystem> {
let wanted = name.trim();
self.subsystems
.iter()
.find(|subsystem| subsystem.name.trim().eq_ignore_ascii_case(wanted))
}
}
struct OpenJoin {
name: JoinName,
opened: usize,
selectors: Vec<SubsystemSelector>,
retained: Vec<RetainedStatement>,
}
struct OpenSubsystem {
name: String,
opened: usize,
implicit: Vec<SubsystemSelector>,
groups: Vec<SelectorGroup>,
retained: Vec<RetainedStatement>,
join: Option<OpenJoin>,
}
impl OpenSubsystem {
fn close(self) -> Subsystem {
let mut groups = Vec::with_capacity(self.groups.len() + 1);
if !self.implicit.is_empty() {
groups.push(SelectorGroup {
join: None,
selectors: self.implicit,
retained: Vec::new(),
});
}
groups.extend(self.groups);
Subsystem {
name: self.name,
groups,
retained: self.retained,
}
}
}
struct Reader {
parsed: SubsystemParsed,
subsystem: Option<OpenSubsystem>,
seen_statement: bool,
ended: bool,
noted_text_after_end: bool,
}
impl Reader {
fn new() -> Self {
Reader {
parsed: SubsystemParsed {
set: SubsystemSet::default(),
diagnostics: Vec::new(),
},
subsystem: None,
seen_statement: false,
ended: false,
noted_text_after_end: false,
}
}
fn read_line(&mut self, line: &LexedLine<'_>) -> Result<()> {
if self.ended {
self.keep_after_end(line);
return Ok(());
}
if !self.take_header(line) {
return Ok(());
}
let upper = line.keywords();
let words = line.words();
if self.subsystem.is_some() {
return self.read_subsystem_line(line, &upper, &words);
}
self.read_file_line(line, &upper, &words);
Ok(())
}
fn finish(self) -> Result<SubsystemParsed> {
if let Some(open) = self.subsystem {
if let Some(join) = open.join {
return Err(bad(format!("line {}: JOIN has no END", join.opened)));
}
return Err(bad(format!(
"line {}: SUBSYSTEM '{}' has no END",
open.opened, open.name
)));
}
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_SUB_TEXT_AFTER_END,
format!("line {}: text follows the file END", line.number),
);
}
self.parsed.set.retained.push(RetainedStatement {
line: line.number,
text: line.trimmed().to_owned(),
after_end: true,
});
}
fn read_file_line(&mut self, line: &LexedLine<'_>, upper: &[String], words: &[&str]) {
match upper[0].as_str() {
"SUBSYSTEM" | "SYSTEM" => self.open_subsystem(line, upper, words),
"END" if upper.len() == 1 => self.ended = true,
_ => {
self.parsed.unrecognized(line.number, line.trimmed());
self.parsed.set.retained.push(RetainedStatement {
line: line.number,
text: line.trimmed().to_owned(),
after_end: false,
});
}
}
}
fn open_subsystem(&mut self, line: &LexedLine<'_>, upper: &[String], words: &[&str]) {
let name = words
.get(1)
.map_or_else(String::new, |word| word.trim().to_owned());
self.subsystem = Some(OpenSubsystem {
name,
opened: line.number,
implicit: Vec::new(),
groups: Vec::new(),
retained: Vec::new(),
join: None,
});
if upper.len() <= 2 {
return;
}
self.read_selector_tail(line, upper, words, 2);
}
fn read_selector_tail(
&mut self,
line: &LexedLine<'_>,
upper: &[String],
words: &[&str],
at: usize,
) {
match take_selectors(upper, at) {
SelectorParse::Read { selectors, ended } => {
self.add_selectors(selectors);
if ended {
self.close_join_or_subsystem();
}
}
SelectorParse::Malformed => {
let tail = tail_text(words, at);
self.parsed.malformed(line.number, &tail);
self.keep_in_subsystem(line.number, tail);
}
SelectorParse::Unrecognized => {
let tail = tail_text(words, at);
self.parsed.unrecognized(line.number, &tail);
self.keep_in_subsystem(line.number, tail);
}
}
}
fn open_join(&mut self, line: &LexedLine<'_>, upper: &[String], words: &[&str]) {
let named = upper
.get(1)
.is_some_and(|word| word != "END" && !is_selector_keyword(word));
let name = if named {
JoinName::Named {
name: words[1].trim().to_owned(),
}
} else {
JoinName::Anonymous
};
let at = usize::from(named) + 1;
if let Some(open) = self.subsystem.as_mut() {
open.join = Some(OpenJoin {
name,
opened: line.number,
selectors: Vec::new(),
retained: Vec::new(),
});
}
if at < upper.len() {
self.read_selector_tail(line, upper, words, at);
}
}
fn read_subsystem_line(
&mut self,
line: &LexedLine<'_>,
upper: &[String],
words: &[&str],
) -> Result<()> {
if is_end(line) {
self.close_join_or_subsystem();
return Ok(());
}
if matches!(upper[0].as_str(), "SUBSYSTEM" | "SYSTEM") {
let name = self
.subsystem
.as_ref()
.map_or("", |open| open.name.as_str());
return Err(bad(format!(
"line {}: SUBSYSTEM starts before subsystem '{name}' reached END",
line.number
)));
}
if upper[0] == "JOIN"
&& self
.subsystem
.as_ref()
.is_some_and(|open| open.join.is_none())
{
self.open_join(line, upper, words);
return Ok(());
}
match take_selectors(upper, 0) {
SelectorParse::Read { selectors, ended } => {
self.add_selectors(selectors);
if ended {
self.close_join_or_subsystem();
}
}
SelectorParse::Malformed => {
self.parsed.malformed(line.number, line.trimmed());
self.keep_in_subsystem(line.number, line.trimmed().to_owned());
}
SelectorParse::Unrecognized => {
self.parsed.unrecognized(line.number, line.trimmed());
self.keep_in_subsystem(line.number, line.trimmed().to_owned());
}
}
Ok(())
}
fn add_selectors(&mut self, selectors: Vec<SubsystemSelector>) {
let Some(open) = self.subsystem.as_mut() else {
return;
};
match open.join.as_mut() {
Some(join) => join.selectors.extend(selectors),
None => open.implicit.extend(selectors),
}
}
fn keep_in_subsystem(&mut self, line: usize, text: String) {
let Some(open) = self.subsystem.as_mut() else {
return;
};
let kept = RetainedStatement {
line,
text,
after_end: false,
};
match open.join.as_mut() {
Some(join) => join.retained.push(kept),
None => open.retained.push(kept),
}
}
fn close_join_or_subsystem(&mut self) {
if let Some(open) = self.subsystem.as_mut()
&& let Some(join) = open.join.take()
{
open.groups.push(SelectorGroup {
join: Some(join.name),
selectors: join.selectors,
retained: join.retained,
});
return;
}
self.close_subsystem();
}
fn close_subsystem(&mut self) {
if let Some(open) = self.subsystem.take() {
self.parsed.set.subsystems.push(open.close());
}
}
}
fn write_retained(out: &mut String, statements: &[RetainedStatement]) {
for statement in statements {
out.push_str(&statement.text);
out.push('\n');
}
}
fn tail_text(words: &[&str], at: usize) -> String {
words[at..]
.iter()
.map(|word| field(word))
.collect::<Vec<String>>()
.join(" ")
}
fn is_end(line: &LexedLine<'_>) -> bool {
line.kind == LineKind::Statement
&& line.tokens.len() == 1
&& line.tokens[0].text.eq_ignore_ascii_case("END")
}
enum SelectorParse {
Read {
selectors: Vec<SubsystemSelector>,
ended: bool,
},
Malformed,
Unrecognized,
}
fn take_selectors(upper: &[String], at: usize) -> SelectorParse {
let mut selectors = Vec::new();
let mut index = at;
while index < upper.len() {
if upper[index] == "END" && index + 1 == upper.len() {
return SelectorParse::Read {
selectors,
ended: true,
};
}
let Some((selector, next)) = take_selector(upper, index) else {
return if index == at && !is_selector_keyword(&upper[index]) {
SelectorParse::Unrecognized
} else {
SelectorParse::Malformed
};
};
selectors.push(selector);
index = next;
}
SelectorParse::Read {
selectors,
ended: false,
}
}
fn is_selector_keyword(word: &str) -> bool {
matches!(
word,
"AREA" | "AREAS" | "ZONE" | "ZONES" | "OWNER" | "OWNERS" | "BUS" | "BUSES" | "KVRANGE"
)
}
fn take_selector(upper: &[String], at: usize) -> Option<(SubsystemSelector, usize)> {
let integer = |offset: usize| upper.get(at + offset)?.parse::<usize>().ok();
let float = |offset: usize| {
upper
.get(at + offset)?
.parse::<f64>()
.ok()
.filter(|value| value.is_finite())
};
match upper[at].as_str() {
"AREA" => Some((
SubsystemSelector::Area {
from: integer(1)?,
to: integer(1)?,
},
at + 2,
)),
"AREAS" => Some((
SubsystemSelector::Area {
from: integer(1)?,
to: integer(2)?,
},
at + 3,
)),
"ZONE" => Some((
SubsystemSelector::Zone {
from: integer(1)?,
to: integer(1)?,
},
at + 2,
)),
"ZONES" => Some((
SubsystemSelector::Zone {
from: integer(1)?,
to: integer(2)?,
},
at + 3,
)),
"OWNER" => Some((
SubsystemSelector::Owner {
from: integer(1)?,
to: integer(1)?,
},
at + 2,
)),
"OWNERS" => Some((
SubsystemSelector::Owner {
from: integer(1)?,
to: integer(2)?,
},
at + 3,
)),
"BUS" => Some((
SubsystemSelector::Bus {
from: BusId(integer(1)?),
to: BusId(integer(1)?),
},
at + 2,
)),
"BUSES" => Some((
SubsystemSelector::Bus {
from: BusId(integer(1)?),
to: BusId(integer(2)?),
},
at + 3,
)),
"KVRANGE" => {
let (lo, hi) = (float(1)?, float(2)?);
(lo <= hi).then_some((SubsystemSelector::KvRange { lo, hi }, at + 3))
}
_ => None,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum SelectorKind {
Area,
Zone,
Owner,
Bus,
Kv,
}
const SELECTOR_KINDS: [SelectorKind; 5] = [
SelectorKind::Area,
SelectorKind::Zone,
SelectorKind::Owner,
SelectorKind::Bus,
SelectorKind::Kv,
];
fn kind_of(selector: &SubsystemSelector) -> SelectorKind {
match selector {
SubsystemSelector::Area { .. } => SelectorKind::Area,
SubsystemSelector::Zone { .. } => SelectorKind::Zone,
SubsystemSelector::Owner { .. } => SelectorKind::Owner,
SubsystemSelector::Bus { .. } => SelectorKind::Bus,
SubsystemSelector::KvRange { .. } => SelectorKind::Kv,
}
}
pub(super) fn owner_of(bus: &Bus) -> usize {
bus.extras
.get("psse_owner")
.and_then(serde_json::Value::as_i64)
.and_then(|owner| usize::try_from(owner).ok())
.unwrap_or(1)
}
fn selects(selector: &SubsystemSelector, bus: &Bus) -> bool {
match selector {
SubsystemSelector::Area { from, to } => (*from..=*to).contains(&bus.area),
SubsystemSelector::Zone { from, to } => (*from..=*to).contains(&bus.zone),
SubsystemSelector::Owner { from, to } => (*from..=*to).contains(&owner_of(bus)),
SubsystemSelector::Bus { from, to } => (from.0..=to.0).contains(&bus.id.0),
SubsystemSelector::KvRange { lo, hi } => bus.base_kv >= *lo && bus.base_kv <= *hi,
}
}
impl Subsystem {
#[must_use]
pub fn select_buses(&self, net: &BalancedNetwork) -> BTreeSet<BusId> {
let mut selected = BTreeSet::new();
for group in &self.groups {
selected.extend(select_group(group, net));
}
selected
}
}
fn select_group(group: &SelectorGroup, net: &BalancedNetwork) -> BTreeSet<BusId> {
let mut selected: Option<BTreeSet<BusId>> = None;
for kind in SELECTOR_KINDS {
let family: Vec<&SubsystemSelector> = group
.selectors
.iter()
.filter(|selector| kind_of(selector) == kind)
.collect();
if family.is_empty() {
continue;
}
let matching: BTreeSet<BusId> = net
.buses()
.iter()
.filter(|bus| family.iter().any(|selector| selects(selector, bus)))
.map(|bus| bus.id)
.collect();
selected = Some(match selected {
None => matching,
Some(held) => held.intersection(&matching).copied().collect(),
});
}
selected.unwrap_or_default()
}
pub(super) fn decimal(value: f64) -> String {
let written = value.to_string();
if written.contains(['.', 'e', 'E', 'i', 'N']) {
written
} else {
format!("{written}.0")
}
}
fn write_selector(selector: &SubsystemSelector) -> String {
match selector {
SubsystemSelector::Area { from, to } if from == to => format!("AREA {from}"),
SubsystemSelector::Area { from, to } => format!("AREAS {from} {to}"),
SubsystemSelector::Zone { from, to } if from == to => format!("ZONE {from}"),
SubsystemSelector::Zone { from, to } => format!("ZONES {from} {to}"),
SubsystemSelector::Owner { from, to } if from == to => format!("OWNER {from}"),
SubsystemSelector::Owner { from, to } => format!("OWNERS {from} {to}"),
SubsystemSelector::Bus { from, to } if from == to => format!("BUS {}", from.0),
SubsystemSelector::Bus { from, to } => format!("BUSES {} {}", from.0, to.0),
SubsystemSelector::KvRange { lo, hi } => {
format!("KVRANGE {} {}", decimal(*lo), decimal(*hi))
}
}
}