Skip to main content

powerio_tx/contingency/
sub.rs

1//! PSS/E subsystem description files (`.sub`).
2//!
3//! A `.sub` file names the bus groups a contingency analysis works over. An
4//! automatic specification in a `.con` file and a monitored element statement
5//! in a `.mon` file both name a subsystem stated here.
6//!
7//! ```text
8//! SUBSYSTEM 'WOA'
9//!    AREA 1
10//! END
11//! END
12//! ```
13//!
14//! [`SubsystemSet::parse`] reads UTF-8 text and touches no filesystem.
15//! Statements outside the grammar keep their trimmed line and are reported,
16//! so a file a tool wrote for itself still reads.
17//! [`SubsystemSet::to_sub`] writes the set back in one canonical spelling.
18//! [`Subsystem::select_buses`] is the separate step that names the buses of a
19//! subsystem in a [`BalancedNetwork`].
20//!
21//! The grammar, its evidence, and the writer's spellings are in `FORMAT.md`
22//! next to this file.
23
24use 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/// One subsystem description file.
35#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
36#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
37pub struct SubsystemSet {
38    /// Comment lines ahead of the first statement, as written. PSS/E leads a
39    /// generated file with its `/PSS(R)E` stamp and a `COM` banner.
40    #[serde(default, skip_serializing_if = "Vec::is_empty")]
41    pub header: Vec<String>,
42    /// The subsystems, in file order.
43    #[serde(default, skip_serializing_if = "Vec::is_empty")]
44    pub subsystems: Vec<Subsystem>,
45    /// File level statements outside the grammar, kept as their trimmed line.
46    #[serde(default, skip_serializing_if = "Vec::is_empty")]
47    pub retained: Vec<RetainedStatement>,
48}
49
50/// One named subsystem: the union of its selector groups.
51#[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    /// The groups whose bus sets are unioned. The implicit group, holding the
56    /// selectors stated outside any `JOIN`, comes first when it has any.
57    #[serde(default, skip_serializing_if = "Vec::is_empty")]
58    pub groups: Vec<SelectorGroup>,
59    /// Statements inside this subsystem, and outside any `JOIN` group, that
60    /// are outside the grammar, kept as their trimmed line.
61    #[serde(default, skip_serializing_if = "Vec::is_empty")]
62    pub retained: Vec<RetainedStatement>,
63}
64
65/// One group of selectors whose bus sets intersect across selector types.
66#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
67#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
68pub struct SelectorGroup {
69    /// How the file stated the group: absent for the implicit group, which
70    /// holds the selectors stated outside any `JOIN`, and present for a `JOIN`
71    /// block whether or not that block states a name. Two `JOIN` blocks with
72    /// no name are two groups, and their bus sets union rather than intersect.
73    #[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    /// Statements read while this `JOIN` was open that are outside the
78    /// grammar, kept as their trimmed line: a nested `JOIN`, a selector line
79    /// whose values are not numbers, and any other unrecognized line. The
80    /// writer states them inside the group, before its `END`, so they read
81    /// back into the same group. The implicit group holds none, because a line
82    /// stated outside any `JOIN` is kept on the subsystem.
83    #[serde(default, skip_serializing_if = "Vec::is_empty")]
84    pub retained: Vec<RetainedStatement>,
85}
86
87/// What a `JOIN` statement stated after the keyword.
88#[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    /// `JOIN` with no name, which the writer states as `JOIN`.
94    Anonymous,
95    /// `JOIN name`.
96    Named { name: String },
97}
98
99/// One bus selector. A statement naming a single value reads as a range whose
100/// `from` and `to` are equal; both ends are inclusive.
101#[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    /// A base kV band, inclusive at both ends.
123    KvRange {
124        lo: f64,
125        hi: f64,
126    },
127}
128
129/// Output of a tolerant subsystem read: the set plus the reader's notes on
130/// statements it kept as text.
131#[derive(Debug, Clone)]
132#[non_exhaustive]
133pub struct SubsystemParsed {
134    pub set: SubsystemSet,
135    /// The reader's notes as structured records.
136    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    /// Read a `.sub` file from UTF-8 text. Keywords are case insensitive.
173    ///
174    /// A statement the grammar does not cover keeps its trimmed line where the
175    /// file stated it, and is reported: in [`SelectorGroup::retained`] inside
176    /// an open `JOIN`, in [`Subsystem::retained`] inside a subsystem, and in
177    /// [`SubsystemSet::retained`] at file level. Lines after a file level
178    /// `END` are kept at file level, marked
179    /// [`RetainedStatement::after_end`], and reported once.
180    ///
181    /// # Errors
182    /// [`Error::FormatRead`] when a `SUBSYSTEM` starts inside another, or when
183    /// a subsystem or a `JOIN` group is still open at end of input. The
184    /// message names the 1-based line.
185    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    /// Write the set as `.sub` text: the header lines as written, the
194    /// subsystems in order with their selectors and `JOIN` groups, the file
195    /// level statements kept from before the file `END`, a final `END`, and
196    /// then the statements read after that `END`. Every line ends with a
197    /// newline.
198    ///
199    /// Each statement kept as text is written where it was read: inside its
200    /// `JOIN` group before that group's `END`, inside its subsystem before the
201    /// subsystem's `END`, or at file level. Reading the result back gives the
202    /// same set, except that a statement kept as text reads back from a
203    /// different line when the writer's order differs from the source's.
204    #[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    /// The subsystem of this name, matched without case and without
254    /// surrounding whitespace, as a `.con` or `.mon` statement names it.
255    #[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
264/// A `JOIN` group whose `END` has not been read yet.
265struct OpenJoin {
266    name: JoinName,
267    opened: usize,
268    selectors: Vec<SubsystemSelector>,
269    retained: Vec<RetainedStatement>,
270}
271
272/// A subsystem whose `END` has not been read yet.
273struct OpenSubsystem {
274    name: String,
275    opened: usize,
276    /// Selectors stated outside any `JOIN`.
277    implicit: Vec<SubsystemSelector>,
278    groups: Vec<SelectorGroup>,
279    retained: Vec<RetainedStatement>,
280    join: Option<OpenJoin>,
281}
282
283impl OpenSubsystem {
284    /// The subsystem as it closes: the implicit group first when it holds any
285    /// selector, then the `JOIN` groups in the order they were read.
286    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
304/// The reader's state while it walks the lines of one file.
305struct Reader {
306    parsed: SubsystemParsed,
307    subsystem: Option<OpenSubsystem>,
308    /// Whether a statement line has been read; comment lines before the first
309    /// one are the file header.
310    seen_statement: bool,
311    /// Whether the file level `END` has been read.
312    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    /// Collect a comment line ahead of the first statement into the header.
361    /// Returns whether the caller should read this line as a statement.
362    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    /// Keep a statement line that follows the file level `END`, and report the
380    /// first one. A further bare `END` states nothing and is dropped, because
381    /// files carry one or two of them.
382    ///
383    /// The statement carries `after_end`, and `to_sub` states it after the
384    /// `END` it writes. A line stated there that opens a subsystem would
385    /// otherwise read back as grammar rather than as text.
386    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    /// Open a subsystem and read the selectors stated on the same line. A
420    /// trailing `END` there closes the subsystem at once.
421    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    /// Read the selectors stated after a `SUBSYSTEM` or `JOIN` keyword and its
440    /// name. A trailing `END` closes what the line opened.
441    ///
442    /// A tail outside the grammar keeps its own line, so that writing the
443    /// subsystem back states the opening keyword and the tail separately and
444    /// reading that again gives the same subsystem.
445    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    /// Open a `JOIN` group and read the rest of its line. The token after the
473    /// keyword is the group's name unless it opens a selector, so `JOIN AREA 1`
474    /// opens a group with no name over area 1.
475    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    /// Keep one line as text where the file stated it: inside the open `JOIN`
559    /// group when there is one, on the subsystem otherwise.
560    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    /// An `END` closes the open `JOIN` group when there is one, and the
576    /// subsystem otherwise.
577    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
598/// Write each kept statement as its own line.
599fn 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
606/// The tokens from `at` to the end of the line, rejoined as one line. A token
607/// holding whitespace is quoted, so the rejoined line lexes back into the same
608/// tokens.
609fn 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
617/// Whether the line is a bare `END`.
618fn 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
624// ---------------------------------------------------------------------------
625// Selector grammar
626// ---------------------------------------------------------------------------
627
628/// What a run of selector tokens read as.
629enum SelectorParse {
630    Read {
631        selectors: Vec<SubsystemSelector>,
632        /// Whether a trailing `END` on the same line closed the subsystem.
633        ended: bool,
634    },
635    /// A selector keyword whose values are not the numbers it needs.
636    Malformed,
637    /// The first token is not a selector keyword.
638    Unrecognized,
639}
640
641/// Read every selector from `at` to the end of the line. A trailing `END`
642/// closes the subsystem, which is how a one line subsystem is written.
643fn 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
669/// Whether the word opens a selector, whatever follows it.
670fn is_selector_keyword(word: &str) -> bool {
671    matches!(
672        word,
673        "AREA" | "AREAS" | "ZONE" | "ZONES" | "OWNER" | "OWNERS" | "BUS" | "BUSES" | "KVRANGE"
674    )
675}
676
677/// One selector starting at `at`, with the index just past it.
678fn 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        // A band whose ends run the wrong way round states no range of base
745        // kV values, so the line stays text rather than reading as a selector
746        // no writer could state again.
747        "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// ---------------------------------------------------------------------------
756// Bus selection
757// ---------------------------------------------------------------------------
758
759/// The selector families a group intersects across.
760#[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
787/// The PSS/E owner of a bus: the retained owner number, or 1 when the source
788/// stated the default the reader drops.
789pub(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    /// The buses of `net` this subsystem names.
809    ///
810    /// Within one group the buses matching each selector family present are
811    /// unioned within the family and intersected across the families, and the
812    /// groups of a subsystem are unioned. A group with no selector, and a
813    /// subsystem with no group, names no bus.
814    #[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
849// ---------------------------------------------------------------------------
850// Writing
851// ---------------------------------------------------------------------------
852
853/// A float in its `Display` form, with `.0` added when that form states no
854/// decimal point, so a written kV or per unit value looks like the ones the
855/// files state and reads back as the same `f64`.
856pub(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}