1use std::collections::BTreeSet;
25
26use super::lexer::{LexedLine, LineKind, lex};
27use super::{RetainedStatement, field, note_within_budget};
28use crate::diagnostics::{Diagnostic, codes};
29use crate::network::{BalancedNetwork, Bus, BusId};
30use crate::{Error, Result};
31
32const FMT: &str = "psse subsystem";
33
34#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
36#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
37pub struct SubsystemSet {
38 #[serde(default, skip_serializing_if = "Vec::is_empty")]
41 pub header: Vec<String>,
42 #[serde(default, skip_serializing_if = "Vec::is_empty")]
44 pub subsystems: Vec<Subsystem>,
45 #[serde(default, skip_serializing_if = "Vec::is_empty")]
47 pub retained: Vec<RetainedStatement>,
48}
49
50#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
52#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
53pub struct Subsystem {
54 pub name: String,
55 #[serde(default, skip_serializing_if = "Vec::is_empty")]
58 pub groups: Vec<SelectorGroup>,
59 #[serde(default, skip_serializing_if = "Vec::is_empty")]
62 pub retained: Vec<RetainedStatement>,
63}
64
65#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
67#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
68pub struct SelectorGroup {
69 #[serde(default, skip_serializing_if = "Option::is_none")]
74 pub join: Option<JoinName>,
75 #[serde(default, skip_serializing_if = "Vec::is_empty")]
76 pub selectors: Vec<SubsystemSelector>,
77 #[serde(default, skip_serializing_if = "Vec::is_empty")]
84 pub retained: Vec<RetainedStatement>,
85}
86
87#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
89#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
90#[serde(tag = "kind", rename_all = "snake_case")]
91#[non_exhaustive]
92pub enum JoinName {
93 Anonymous,
95 Named { name: String },
97}
98
99#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
102#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
103#[serde(tag = "kind", rename_all = "snake_case")]
104#[non_exhaustive]
105pub enum SubsystemSelector {
106 Area {
107 from: usize,
108 to: usize,
109 },
110 Zone {
111 from: usize,
112 to: usize,
113 },
114 Owner {
115 from: usize,
116 to: usize,
117 },
118 Bus {
119 from: BusId,
120 to: BusId,
121 },
122 KvRange {
124 lo: f64,
125 hi: f64,
126 },
127}
128
129#[derive(Debug, Clone)]
132#[non_exhaustive]
133pub struct SubsystemParsed {
134 pub set: SubsystemSet,
135 pub diagnostics: Vec<Diagnostic>,
137}
138
139impl SubsystemParsed {
140 fn note(&mut self, info: &'static crate::diagnostics::DiagnosticInfo, message: String) {
141 note_within_budget(
142 &mut self.diagnostics,
143 info,
144 &codes::READ_SUB_NOTES_TRUNCATED,
145 message,
146 );
147 }
148
149 fn unrecognized(&mut self, number: usize, text: &str) {
150 self.note(
151 &codes::READ_SUB_STATEMENT_UNRECOGNIZED,
152 format!("line {number}: statement kept as text: {text}"),
153 );
154 }
155
156 fn malformed(&mut self, number: usize, text: &str) {
157 self.note(
158 &codes::READ_SUB_SOURCE_MALFORMED,
159 format!("line {number}: a selector states no number and was kept as text: {text}"),
160 );
161 }
162}
163
164fn bad(message: String) -> Error {
165 Error::FormatRead {
166 format: FMT,
167 message,
168 }
169}
170
171impl SubsystemSet {
172 pub fn parse(text: &str) -> Result<SubsystemParsed> {
186 let mut reader = Reader::new();
187 for line in lex(text) {
188 reader.read_line(&line)?;
189 }
190 reader.finish()
191 }
192
193 #[must_use]
205 pub fn to_sub(&self) -> String {
206 use std::fmt::Write as _;
207
208 let mut out = String::new();
209 for line in &self.header {
210 out.push_str(line);
211 out.push('\n');
212 }
213 for subsystem in &self.subsystems {
214 let _ = writeln!(out, "SUBSYSTEM '{}'", subsystem.name);
215 for group in &subsystem.groups {
216 match &group.join {
217 None => {
218 for selector in &group.selectors {
219 let _ = writeln!(out, " {}", write_selector(selector));
220 }
221 write_retained(&mut out, &group.retained);
222 }
223 Some(join) => {
224 match join {
225 JoinName::Anonymous => out.push_str(" JOIN\n"),
226 JoinName::Named { name } => {
227 let _ = writeln!(out, " JOIN '{name}'");
228 }
229 }
230 for selector in &group.selectors {
231 let _ = writeln!(out, " {}", write_selector(selector));
232 }
233 write_retained(&mut out, &group.retained);
234 out.push_str(" END\n");
235 }
236 }
237 }
238 write_retained(&mut out, &subsystem.retained);
239 out.push_str("END\n");
240 }
241 for statement in self.retained.iter().filter(|kept| !kept.after_end) {
242 out.push_str(&statement.text);
243 out.push('\n');
244 }
245 out.push_str("END\n");
246 for statement in self.retained.iter().filter(|kept| kept.after_end) {
247 out.push_str(&statement.text);
248 out.push('\n');
249 }
250 out
251 }
252
253 #[must_use]
256 pub fn get(&self, name: &str) -> Option<&Subsystem> {
257 let wanted = name.trim();
258 self.subsystems
259 .iter()
260 .find(|subsystem| subsystem.name.trim().eq_ignore_ascii_case(wanted))
261 }
262}
263
264struct OpenJoin {
266 name: JoinName,
267 opened: usize,
268 selectors: Vec<SubsystemSelector>,
269 retained: Vec<RetainedStatement>,
270}
271
272struct OpenSubsystem {
274 name: String,
275 opened: usize,
276 implicit: Vec<SubsystemSelector>,
278 groups: Vec<SelectorGroup>,
279 retained: Vec<RetainedStatement>,
280 join: Option<OpenJoin>,
281}
282
283impl OpenSubsystem {
284 fn close(self) -> Subsystem {
287 let mut groups = Vec::with_capacity(self.groups.len() + 1);
288 if !self.implicit.is_empty() {
289 groups.push(SelectorGroup {
290 join: None,
291 selectors: self.implicit,
292 retained: Vec::new(),
293 });
294 }
295 groups.extend(self.groups);
296 Subsystem {
297 name: self.name,
298 groups,
299 retained: self.retained,
300 }
301 }
302}
303
304struct Reader {
306 parsed: SubsystemParsed,
307 subsystem: Option<OpenSubsystem>,
308 seen_statement: bool,
311 ended: bool,
313 noted_text_after_end: bool,
314}
315
316impl Reader {
317 fn new() -> Self {
318 Reader {
319 parsed: SubsystemParsed {
320 set: SubsystemSet::default(),
321 diagnostics: Vec::new(),
322 },
323 subsystem: None,
324 seen_statement: false,
325 ended: false,
326 noted_text_after_end: false,
327 }
328 }
329
330 fn read_line(&mut self, line: &LexedLine<'_>) -> Result<()> {
331 if self.ended {
332 self.keep_after_end(line);
333 return Ok(());
334 }
335 if !self.take_header(line) {
336 return Ok(());
337 }
338 let upper = line.keywords();
339 let words = line.words();
340 if self.subsystem.is_some() {
341 return self.read_subsystem_line(line, &upper, &words);
342 }
343 self.read_file_line(line, &upper, &words);
344 Ok(())
345 }
346
347 fn finish(self) -> Result<SubsystemParsed> {
348 if let Some(open) = self.subsystem {
349 if let Some(join) = open.join {
350 return Err(bad(format!("line {}: JOIN has no END", join.opened)));
351 }
352 return Err(bad(format!(
353 "line {}: SUBSYSTEM '{}' has no END",
354 open.opened, open.name
355 )));
356 }
357 Ok(self.parsed)
358 }
359
360 fn take_header(&mut self, line: &LexedLine<'_>) -> bool {
363 if self.seen_statement {
364 return line.kind == LineKind::Statement;
365 }
366 match line.kind {
367 LineKind::Blank => false,
368 LineKind::Comment => {
369 self.parsed.set.header.push(line.text.to_owned());
370 false
371 }
372 LineKind::Statement => {
373 self.seen_statement = true;
374 true
375 }
376 }
377 }
378
379 fn keep_after_end(&mut self, line: &LexedLine<'_>) {
387 if line.kind != LineKind::Statement || is_end(line) {
388 return;
389 }
390 if !self.noted_text_after_end {
391 self.noted_text_after_end = true;
392 self.parsed.note(
393 &codes::READ_SUB_TEXT_AFTER_END,
394 format!("line {}: text follows the file END", line.number),
395 );
396 }
397 self.parsed.set.retained.push(RetainedStatement {
398 line: line.number,
399 text: line.trimmed().to_owned(),
400 after_end: true,
401 });
402 }
403
404 fn read_file_line(&mut self, line: &LexedLine<'_>, upper: &[String], words: &[&str]) {
405 match upper[0].as_str() {
406 "SUBSYSTEM" | "SYSTEM" => self.open_subsystem(line, upper, words),
407 "END" if upper.len() == 1 => self.ended = true,
408 _ => {
409 self.parsed.unrecognized(line.number, line.trimmed());
410 self.parsed.set.retained.push(RetainedStatement {
411 line: line.number,
412 text: line.trimmed().to_owned(),
413 after_end: false,
414 });
415 }
416 }
417 }
418
419 fn open_subsystem(&mut self, line: &LexedLine<'_>, upper: &[String], words: &[&str]) {
422 let name = words
423 .get(1)
424 .map_or_else(String::new, |word| word.trim().to_owned());
425 self.subsystem = Some(OpenSubsystem {
426 name,
427 opened: line.number,
428 implicit: Vec::new(),
429 groups: Vec::new(),
430 retained: Vec::new(),
431 join: None,
432 });
433 if upper.len() <= 2 {
434 return;
435 }
436 self.read_selector_tail(line, upper, words, 2);
437 }
438
439 fn read_selector_tail(
446 &mut self,
447 line: &LexedLine<'_>,
448 upper: &[String],
449 words: &[&str],
450 at: usize,
451 ) {
452 match take_selectors(upper, at) {
453 SelectorParse::Read { selectors, ended } => {
454 self.add_selectors(selectors);
455 if ended {
456 self.close_join_or_subsystem();
457 }
458 }
459 SelectorParse::Malformed => {
460 let tail = tail_text(words, at);
461 self.parsed.malformed(line.number, &tail);
462 self.keep_in_subsystem(line.number, tail);
463 }
464 SelectorParse::Unrecognized => {
465 let tail = tail_text(words, at);
466 self.parsed.unrecognized(line.number, &tail);
467 self.keep_in_subsystem(line.number, tail);
468 }
469 }
470 }
471
472 fn open_join(&mut self, line: &LexedLine<'_>, upper: &[String], words: &[&str]) {
476 let named = upper
477 .get(1)
478 .is_some_and(|word| word != "END" && !is_selector_keyword(word));
479 let name = if named {
480 JoinName::Named {
481 name: words[1].trim().to_owned(),
482 }
483 } else {
484 JoinName::Anonymous
485 };
486 let at = usize::from(named) + 1;
487 if let Some(open) = self.subsystem.as_mut() {
488 open.join = Some(OpenJoin {
489 name,
490 opened: line.number,
491 selectors: Vec::new(),
492 retained: Vec::new(),
493 });
494 }
495 if at < upper.len() {
496 self.read_selector_tail(line, upper, words, at);
497 }
498 }
499
500 fn read_subsystem_line(
501 &mut self,
502 line: &LexedLine<'_>,
503 upper: &[String],
504 words: &[&str],
505 ) -> Result<()> {
506 if is_end(line) {
507 self.close_join_or_subsystem();
508 return Ok(());
509 }
510 if matches!(upper[0].as_str(), "SUBSYSTEM" | "SYSTEM") {
511 let name = self
512 .subsystem
513 .as_ref()
514 .map_or("", |open| open.name.as_str());
515 return Err(bad(format!(
516 "line {}: SUBSYSTEM starts before subsystem '{name}' reached END",
517 line.number
518 )));
519 }
520 if upper[0] == "JOIN"
521 && self
522 .subsystem
523 .as_ref()
524 .is_some_and(|open| open.join.is_none())
525 {
526 self.open_join(line, upper, words);
527 return Ok(());
528 }
529 match take_selectors(upper, 0) {
530 SelectorParse::Read { selectors, ended } => {
531 self.add_selectors(selectors);
532 if ended {
533 self.close_join_or_subsystem();
534 }
535 }
536 SelectorParse::Malformed => {
537 self.parsed.malformed(line.number, line.trimmed());
538 self.keep_in_subsystem(line.number, line.trimmed().to_owned());
539 }
540 SelectorParse::Unrecognized => {
541 self.parsed.unrecognized(line.number, line.trimmed());
542 self.keep_in_subsystem(line.number, line.trimmed().to_owned());
543 }
544 }
545 Ok(())
546 }
547
548 fn add_selectors(&mut self, selectors: Vec<SubsystemSelector>) {
549 let Some(open) = self.subsystem.as_mut() else {
550 return;
551 };
552 match open.join.as_mut() {
553 Some(join) => join.selectors.extend(selectors),
554 None => open.implicit.extend(selectors),
555 }
556 }
557
558 fn keep_in_subsystem(&mut self, line: usize, text: String) {
561 let Some(open) = self.subsystem.as_mut() else {
562 return;
563 };
564 let kept = RetainedStatement {
565 line,
566 text,
567 after_end: false,
568 };
569 match open.join.as_mut() {
570 Some(join) => join.retained.push(kept),
571 None => open.retained.push(kept),
572 }
573 }
574
575 fn close_join_or_subsystem(&mut self) {
578 if let Some(open) = self.subsystem.as_mut()
579 && let Some(join) = open.join.take()
580 {
581 open.groups.push(SelectorGroup {
582 join: Some(join.name),
583 selectors: join.selectors,
584 retained: join.retained,
585 });
586 return;
587 }
588 self.close_subsystem();
589 }
590
591 fn close_subsystem(&mut self) {
592 if let Some(open) = self.subsystem.take() {
593 self.parsed.set.subsystems.push(open.close());
594 }
595 }
596}
597
598fn write_retained(out: &mut String, statements: &[RetainedStatement]) {
600 for statement in statements {
601 out.push_str(&statement.text);
602 out.push('\n');
603 }
604}
605
606fn tail_text(words: &[&str], at: usize) -> String {
610 words[at..]
611 .iter()
612 .map(|word| field(word))
613 .collect::<Vec<String>>()
614 .join(" ")
615}
616
617fn is_end(line: &LexedLine<'_>) -> bool {
619 line.kind == LineKind::Statement
620 && line.tokens.len() == 1
621 && line.tokens[0].text.eq_ignore_ascii_case("END")
622}
623
624enum SelectorParse {
630 Read {
631 selectors: Vec<SubsystemSelector>,
632 ended: bool,
634 },
635 Malformed,
637 Unrecognized,
639}
640
641fn take_selectors(upper: &[String], at: usize) -> SelectorParse {
644 let mut selectors = Vec::new();
645 let mut index = at;
646 while index < upper.len() {
647 if upper[index] == "END" && index + 1 == upper.len() {
648 return SelectorParse::Read {
649 selectors,
650 ended: true,
651 };
652 }
653 let Some((selector, next)) = take_selector(upper, index) else {
654 return if index == at && !is_selector_keyword(&upper[index]) {
655 SelectorParse::Unrecognized
656 } else {
657 SelectorParse::Malformed
658 };
659 };
660 selectors.push(selector);
661 index = next;
662 }
663 SelectorParse::Read {
664 selectors,
665 ended: false,
666 }
667}
668
669fn is_selector_keyword(word: &str) -> bool {
671 matches!(
672 word,
673 "AREA" | "AREAS" | "ZONE" | "ZONES" | "OWNER" | "OWNERS" | "BUS" | "BUSES" | "KVRANGE"
674 )
675}
676
677fn take_selector(upper: &[String], at: usize) -> Option<(SubsystemSelector, usize)> {
679 let integer = |offset: usize| upper.get(at + offset)?.parse::<usize>().ok();
680 let float = |offset: usize| {
681 upper
682 .get(at + offset)?
683 .parse::<f64>()
684 .ok()
685 .filter(|value| value.is_finite())
686 };
687 match upper[at].as_str() {
688 "AREA" => Some((
689 SubsystemSelector::Area {
690 from: integer(1)?,
691 to: integer(1)?,
692 },
693 at + 2,
694 )),
695 "AREAS" => Some((
696 SubsystemSelector::Area {
697 from: integer(1)?,
698 to: integer(2)?,
699 },
700 at + 3,
701 )),
702 "ZONE" => Some((
703 SubsystemSelector::Zone {
704 from: integer(1)?,
705 to: integer(1)?,
706 },
707 at + 2,
708 )),
709 "ZONES" => Some((
710 SubsystemSelector::Zone {
711 from: integer(1)?,
712 to: integer(2)?,
713 },
714 at + 3,
715 )),
716 "OWNER" => Some((
717 SubsystemSelector::Owner {
718 from: integer(1)?,
719 to: integer(1)?,
720 },
721 at + 2,
722 )),
723 "OWNERS" => Some((
724 SubsystemSelector::Owner {
725 from: integer(1)?,
726 to: integer(2)?,
727 },
728 at + 3,
729 )),
730 "BUS" => Some((
731 SubsystemSelector::Bus {
732 from: BusId(integer(1)?),
733 to: BusId(integer(1)?),
734 },
735 at + 2,
736 )),
737 "BUSES" => Some((
738 SubsystemSelector::Bus {
739 from: BusId(integer(1)?),
740 to: BusId(integer(2)?),
741 },
742 at + 3,
743 )),
744 "KVRANGE" => {
748 let (lo, hi) = (float(1)?, float(2)?);
749 (lo <= hi).then_some((SubsystemSelector::KvRange { lo, hi }, at + 3))
750 }
751 _ => None,
752 }
753}
754
755#[derive(Debug, Clone, Copy, PartialEq, Eq)]
761enum SelectorKind {
762 Area,
763 Zone,
764 Owner,
765 Bus,
766 Kv,
767}
768
769const SELECTOR_KINDS: [SelectorKind; 5] = [
770 SelectorKind::Area,
771 SelectorKind::Zone,
772 SelectorKind::Owner,
773 SelectorKind::Bus,
774 SelectorKind::Kv,
775];
776
777fn kind_of(selector: &SubsystemSelector) -> SelectorKind {
778 match selector {
779 SubsystemSelector::Area { .. } => SelectorKind::Area,
780 SubsystemSelector::Zone { .. } => SelectorKind::Zone,
781 SubsystemSelector::Owner { .. } => SelectorKind::Owner,
782 SubsystemSelector::Bus { .. } => SelectorKind::Bus,
783 SubsystemSelector::KvRange { .. } => SelectorKind::Kv,
784 }
785}
786
787pub(super) fn owner_of(bus: &Bus) -> usize {
790 bus.extras
791 .get("psse_owner")
792 .and_then(serde_json::Value::as_i64)
793 .and_then(|owner| usize::try_from(owner).ok())
794 .unwrap_or(1)
795}
796
797fn selects(selector: &SubsystemSelector, bus: &Bus) -> bool {
798 match selector {
799 SubsystemSelector::Area { from, to } => (*from..=*to).contains(&bus.area),
800 SubsystemSelector::Zone { from, to } => (*from..=*to).contains(&bus.zone),
801 SubsystemSelector::Owner { from, to } => (*from..=*to).contains(&owner_of(bus)),
802 SubsystemSelector::Bus { from, to } => (from.0..=to.0).contains(&bus.id.0),
803 SubsystemSelector::KvRange { lo, hi } => bus.base_kv >= *lo && bus.base_kv <= *hi,
804 }
805}
806
807impl Subsystem {
808 #[must_use]
815 pub fn select_buses(&self, net: &BalancedNetwork) -> BTreeSet<BusId> {
816 let mut selected = BTreeSet::new();
817 for group in &self.groups {
818 selected.extend(select_group(group, net));
819 }
820 selected
821 }
822}
823
824fn select_group(group: &SelectorGroup, net: &BalancedNetwork) -> BTreeSet<BusId> {
825 let mut selected: Option<BTreeSet<BusId>> = None;
826 for kind in SELECTOR_KINDS {
827 let family: Vec<&SubsystemSelector> = group
828 .selectors
829 .iter()
830 .filter(|selector| kind_of(selector) == kind)
831 .collect();
832 if family.is_empty() {
833 continue;
834 }
835 let matching: BTreeSet<BusId> = net
836 .buses()
837 .iter()
838 .filter(|bus| family.iter().any(|selector| selects(selector, bus)))
839 .map(|bus| bus.id)
840 .collect();
841 selected = Some(match selected {
842 None => matching,
843 Some(held) => held.intersection(&matching).copied().collect(),
844 });
845 }
846 selected.unwrap_or_default()
847}
848
849pub(super) fn decimal(value: f64) -> String {
857 let written = value.to_string();
858 if written.contains(['.', 'e', 'E', 'i', 'N']) {
859 written
860 } else {
861 format!("{written}.0")
862 }
863}
864
865fn write_selector(selector: &SubsystemSelector) -> String {
866 match selector {
867 SubsystemSelector::Area { from, to } if from == to => format!("AREA {from}"),
868 SubsystemSelector::Area { from, to } => format!("AREAS {from} {to}"),
869 SubsystemSelector::Zone { from, to } if from == to => format!("ZONE {from}"),
870 SubsystemSelector::Zone { from, to } => format!("ZONES {from} {to}"),
871 SubsystemSelector::Owner { from, to } if from == to => format!("OWNER {from}"),
872 SubsystemSelector::Owner { from, to } => format!("OWNERS {from} {to}"),
873 SubsystemSelector::Bus { from, to } if from == to => format!("BUS {}", from.0),
874 SubsystemSelector::Bus { from, to } => format!("BUSES {} {}", from.0, to.0),
875 SubsystemSelector::KvRange { lo, hi } => {
876 format!("KVRANGE {} {}", decimal(*lo), decimal(*hi))
877 }
878 }
879}