Skip to main content

powerio_tx/contingency/
mon.rs

1//! PSS/E monitored element files (`.mon`).
2//!
3//! A `.mon` file names what a contingency analysis reports on: branch flows,
4//! interface flows, and bus voltages. Every scope that names a subsystem is
5//! resolved against a `.sub` file's [`SubsystemSet`].
6//!
7//! ```text
8//! MONITOR VOLTAGE RANGE SUBSYSTEM 'ILLINOIS200' 0.950 1.050
9//! MONITOR BRANCHES IN SUBSYSTEM 'ILLINOIS200'
10//! MONITOR TIES FROM SUBSYSTEM 'ILLINOIS200'
11//! END
12//! ```
13//!
14//! [`MonitoredSet::parse`] reads UTF-8 text and touches no filesystem.
15//! Statements outside the grammar keep their trimmed line and are reported.
16//! [`MonitoredSet::to_mon`] writes the set back in one canonical spelling, and
17//! [`MonitoredSet::resolve`] binds it to the rows of a [`BalancedNetwork`].
18//!
19//! The grammar, its evidence, and the writer's spellings are in `FORMAT.md`
20//! next to this file.
21
22use std::collections::BTreeSet;
23
24use super::expand::low_voltage_bus;
25use super::lexer::{LexedLine, LineKind, lex};
26use super::sub::{SubsystemSet, decimal};
27use super::{PsseEquipmentIndex, RetainedStatement, field, note_within_budget};
28use crate::diagnostics::{Diagnostic, codes};
29use crate::network::{BalancedNetwork, BusId};
30use crate::{Error, Result};
31
32const FMT: &str = "psse monitored elements";
33
34/// Base kV values this far apart name the same voltage level.
35const KV_TOLERANCE: f64 = 1e-6;
36
37/// One monitored element file.
38#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
39#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
40pub struct MonitoredSet {
41    /// Comment lines ahead of the first statement, as written.
42    #[serde(default, skip_serializing_if = "Vec::is_empty")]
43    pub header: Vec<String>,
44    /// The monitor statements, in file order.
45    #[serde(default, skip_serializing_if = "Vec::is_empty")]
46    pub statements: Vec<MonitorStatement>,
47    /// Statements outside the grammar, kept as their trimmed line.
48    #[serde(default, skip_serializing_if = "Vec::is_empty")]
49    pub retained: Vec<RetainedStatement>,
50}
51
52/// One monitor statement.
53#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
54#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
55#[serde(tag = "kind", rename_all = "snake_case")]
56#[non_exhaustive]
57pub enum MonitorStatement {
58    /// Every branch with both terminals in the subsystem.
59    BranchesInSubsystem {
60        subsystem: String,
61        /// `3WLOWVOLTAGE`: also the three winding transformers whose lowest
62        /// voltage winding sits in the subsystem.
63        low_voltage_3w: bool,
64    },
65    /// Every branch with exactly one terminal in the subsystem.
66    TiesFromSubsystem { subsystem: String },
67    /// The branches a `MONITOR BRANCHES` block lists by terminal pair.
68    Branches {
69        branches: Vec<BranchRef>,
70        /// The block's lines that state no branch, kept as text. The writer
71        /// states them inside the block, before its `END`.
72        #[serde(default, skip_serializing_if = "Vec::is_empty")]
73        retained: Vec<RetainedStatement>,
74    },
75    /// A named interface and the branches whose flows sum over it.
76    Interface {
77        name: String,
78        #[serde(default, skip_serializing_if = "Option::is_none")]
79        rating_mw: Option<f64>,
80        #[serde(default, skip_serializing_if = "Vec::is_empty")]
81        branches: Vec<BranchRef>,
82        /// The block's lines that state no branch, kept as text. The writer
83        /// states them inside the block, before its `END`.
84        #[serde(default, skip_serializing_if = "Vec::is_empty")]
85        retained: Vec<RetainedStatement>,
86    },
87    /// A voltage magnitude band over a scope of buses.
88    VoltageRange {
89        scope: MonitorScope,
90        vmin: f64,
91        vmax: f64,
92    },
93    /// A voltage deviation band over a scope of buses. A statement naming one
94    /// value states the downward limit alone.
95    VoltageDeviation {
96        scope: MonitorScope,
97        down: f64,
98        #[serde(default, skip_serializing_if = "Option::is_none")]
99        up: Option<f64>,
100    },
101}
102
103/// One branch named by its terminal buses and circuit id.
104#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
105#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
106pub struct BranchRef {
107    pub from: BusId,
108    pub to: BusId,
109    /// An absent circuit id in the source reads as `1`.
110    pub circuit: String,
111}
112
113/// The buses a voltage statement applies to.
114#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
115#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
116#[serde(tag = "kind", rename_all = "snake_case")]
117#[non_exhaustive]
118pub enum MonitorScope {
119    AllBuses,
120    Subsystem {
121        name: String,
122    },
123    Bus {
124        bus: BusId,
125    },
126    Area {
127        area: usize,
128    },
129    Zone {
130        zone: usize,
131    },
132    Owner {
133        owner: usize,
134    },
135    /// Every bus at this base kV.
136    Kv {
137        kv: f64,
138    },
139}
140
141/// Output of a tolerant monitored element read.
142#[derive(Debug, Clone)]
143#[non_exhaustive]
144pub struct MonitoredParsed {
145    pub set: MonitoredSet,
146    /// The reader's notes as structured records.
147    pub diagnostics: Vec<Diagnostic>,
148}
149
150impl MonitoredParsed {
151    fn note(&mut self, info: &'static crate::diagnostics::DiagnosticInfo, message: String) {
152        note_within_budget(
153            &mut self.diagnostics,
154            info,
155            &codes::READ_MON_NOTES_TRUNCATED,
156            message,
157        );
158    }
159
160    fn unrecognized(&mut self, number: usize, text: &str) {
161        self.note(
162            &codes::READ_MON_STATEMENT_UNRECOGNIZED,
163            format!("line {number}: statement kept as text: {text}"),
164        );
165    }
166}
167
168fn bad(message: String) -> Error {
169    Error::FormatRead {
170        format: FMT,
171        message,
172    }
173}
174
175impl MonitoredSet {
176    /// Read a `.mon` file from UTF-8 text. Keywords are case insensitive.
177    ///
178    /// A statement the grammar does not cover keeps its trimmed line where the
179    /// file stated it, and is reported: on the statement inside an open
180    /// `MONITOR BRANCHES` or `MONITOR INTERFACE` block, and in
181    /// [`MonitoredSet::retained`] outside one. Lines after a file level `END`
182    /// are kept at file level, marked [`RetainedStatement::after_end`], and
183    /// reported once.
184    ///
185    /// # Errors
186    /// [`Error::FormatRead`] when a `MONITOR BRANCHES` or `MONITOR INTERFACE`
187    /// block is still open at end of input. The message names the 1-based
188    /// line.
189    pub fn parse(text: &str) -> Result<MonitoredParsed> {
190        let mut reader = Reader::new();
191        for line in lex(text) {
192            reader.read_line(&line);
193        }
194        reader.finish()
195    }
196
197    /// Write the set as `.mon` text: the header lines as written, the
198    /// statements in order, the statements kept from before the file `END`, a
199    /// final `END`, and then the statements read after that `END`. Every line
200    /// ends with a newline.
201    ///
202    /// A line kept from inside a block is written inside that block, before
203    /// its `END`, so it reads back into the same block.
204    #[must_use]
205    pub fn to_mon(&self) -> String {
206        let mut out = String::new();
207        for line in &self.header {
208            out.push_str(line);
209            out.push('\n');
210        }
211        for statement in &self.statements {
212            out.push_str(&write_statement(statement));
213        }
214        for statement in self.retained.iter().filter(|kept| !kept.after_end) {
215            out.push_str(&statement.text);
216            out.push('\n');
217        }
218        out.push_str("END\n");
219        for statement in self.retained.iter().filter(|kept| kept.after_end) {
220            out.push_str(&statement.text);
221            out.push('\n');
222        }
223        out
224    }
225}
226
227/// A `MONITOR BRANCHES` or `MONITOR INTERFACE` block whose `END` has not been
228/// read yet.
229struct OpenBlock {
230    opened: usize,
231    /// The interface this block belongs to, or `None` for a bare branch list.
232    interface: Option<(String, Option<f64>)>,
233    branches: Vec<BranchRef>,
234    retained: Vec<RetainedStatement>,
235}
236
237/// The reader's state while it walks the lines of one file.
238struct Reader {
239    parsed: MonitoredParsed,
240    block: Option<OpenBlock>,
241    seen_statement: bool,
242    ended: bool,
243    noted_text_after_end: bool,
244}
245
246impl Reader {
247    fn new() -> Self {
248        Reader {
249            parsed: MonitoredParsed {
250                set: MonitoredSet::default(),
251                diagnostics: Vec::new(),
252            },
253            block: None,
254            seen_statement: false,
255            ended: false,
256            noted_text_after_end: false,
257        }
258    }
259
260    fn read_line(&mut self, line: &LexedLine<'_>) {
261        if self.block.is_some() {
262            self.read_block_line(line);
263            return;
264        }
265        if self.ended {
266            self.keep_after_end(line);
267            return;
268        }
269        if !self.take_header(line) {
270            return;
271        }
272        let upper = line.keywords();
273        let words = line.words();
274        self.read_statement(line, &upper, &words);
275    }
276
277    fn finish(self) -> Result<MonitoredParsed> {
278        if let Some(open) = self.block {
279            let what = match open.interface {
280                Some((name, _)) => format!("MONITOR INTERFACE '{name}'"),
281                None => "MONITOR BRANCHES".to_owned(),
282            };
283            return Err(bad(format!("line {}: {what} has no END", open.opened)));
284        }
285        Ok(self.parsed)
286    }
287
288    /// Collect a comment line ahead of the first statement into the header.
289    /// Returns whether the caller should read this line as a statement.
290    fn take_header(&mut self, line: &LexedLine<'_>) -> bool {
291        if self.seen_statement {
292            return line.kind == LineKind::Statement;
293        }
294        match line.kind {
295            LineKind::Blank => false,
296            LineKind::Comment => {
297                self.parsed.set.header.push(line.text.to_owned());
298                false
299            }
300            LineKind::Statement => {
301                self.seen_statement = true;
302                true
303            }
304        }
305    }
306
307    /// Keep a statement line that follows the file level `END`, and report the
308    /// first one. A further bare `END` states nothing and is dropped, because
309    /// files carry one or two of them.
310    ///
311    /// The statement carries `after_end`, and `to_mon` states it after the
312    /// `END` it writes. A line stated there that opens a monitor statement
313    /// would otherwise read back as grammar rather than as text.
314    fn keep_after_end(&mut self, line: &LexedLine<'_>) {
315        if line.kind != LineKind::Statement || is_end(line) {
316            return;
317        }
318        if !self.noted_text_after_end {
319            self.noted_text_after_end = true;
320            self.parsed.note(
321                &codes::READ_MON_TEXT_AFTER_END,
322                format!("line {}: text follows the file END", line.number),
323            );
324        }
325        self.keep_statement(line, true);
326    }
327
328    /// Keep one line as a file level statement. `after_end` marks a line the
329    /// file states after its `END`, which the writer states after the `END` it
330    /// writes.
331    fn keep_statement(&mut self, line: &LexedLine<'_>, after_end: bool) {
332        self.parsed.set.retained.push(RetainedStatement {
333            line: line.number,
334            text: line.trimmed().to_owned(),
335            after_end,
336        });
337    }
338
339    /// Add one branch to the open block, and close the block on its `END`.
340    fn read_block_line(&mut self, line: &LexedLine<'_>) {
341        if line.kind != LineKind::Statement {
342            return;
343        }
344        if is_end(line) {
345            let Some(open) = self.block.take() else {
346                return;
347            };
348            self.parsed.set.statements.push(match open.interface {
349                Some((name, rating_mw)) => MonitorStatement::Interface {
350                    name,
351                    rating_mw,
352                    branches: open.branches,
353                    retained: open.retained,
354                },
355                None => MonitorStatement::Branches {
356                    branches: open.branches,
357                    retained: open.retained,
358                },
359            });
360            return;
361        }
362        let words = line.words();
363        if let Some(branch) = parse_branch_ref(&words) {
364            if let Some(open) = self.block.as_mut() {
365                open.branches.push(branch);
366            }
367            return;
368        }
369        self.parsed.note(
370            &codes::READ_MON_SOURCE_MALFORMED,
371            format!(
372                "line {}: a monitored block line states no branch and was kept as text: {}",
373                line.number,
374                line.trimmed()
375            ),
376        );
377        if let Some(open) = self.block.as_mut() {
378            open.retained.push(RetainedStatement {
379                line: line.number,
380                text: line.trimmed().to_owned(),
381                after_end: false,
382            });
383        }
384    }
385
386    fn read_statement(&mut self, line: &LexedLine<'_>, upper: &[String], words: &[&str]) {
387        if is_end(line) {
388            self.ended = true;
389            return;
390        }
391        if upper[0] == "MONITOR"
392            && let Some(read) = self.read_monitor(line, upper, words)
393        {
394            match read {
395                Read::Statement(statement) => self.parsed.set.statements.push(statement),
396                Read::OpenedBlock => {}
397            }
398            return;
399        }
400        self.parsed.unrecognized(line.number, line.trimmed());
401        self.keep_statement(line, false);
402    }
403
404    /// One `MONITOR` statement, or `None` when the tail is outside the
405    /// grammar.
406    fn read_monitor(
407        &mut self,
408        line: &LexedLine<'_>,
409        upper: &[String],
410        words: &[&str],
411    ) -> Option<Read> {
412        match upper.get(1)?.as_str() {
413            "BRANCHES" | "LINES" => {
414                if upper.len() == 2 {
415                    self.block = Some(OpenBlock {
416                        opened: line.number,
417                        interface: None,
418                        branches: Vec::new(),
419                        retained: Vec::new(),
420                    });
421                    return Some(Read::OpenedBlock);
422                }
423                let (subsystem, low_voltage_3w) = parse_in_subsystem(upper, words, 2)?;
424                Some(Read::Statement(MonitorStatement::BranchesInSubsystem {
425                    subsystem,
426                    low_voltage_3w,
427                }))
428            }
429            "TIES" => {
430                let (subsystem, low_voltage_3w) = parse_in_subsystem(upper, words, 2)?;
431                (!low_voltage_3w).then_some(Read::Statement(MonitorStatement::TiesFromSubsystem {
432                    subsystem,
433                }))
434            }
435            "INTERFACE" => {
436                let name = words.get(2)?.trim().to_owned();
437                let rating_mw = match parse_rating(upper, 3) {
438                    Rating::Absent => None,
439                    Rating::Stated(value) => Some(value),
440                    Rating::Unreadable => return None,
441                };
442                self.block = Some(OpenBlock {
443                    opened: line.number,
444                    interface: Some((name, rating_mw)),
445                    branches: Vec::new(),
446                    retained: Vec::new(),
447                });
448                Some(Read::OpenedBlock)
449            }
450            "VOLTAGE" => parse_voltage(upper, words).map(Read::Statement),
451            _ => None,
452        }
453    }
454}
455
456/// What one `MONITOR` line produced.
457enum Read {
458    Statement(MonitorStatement),
459    OpenedBlock,
460}
461
462/// Whether the line is a bare `END`.
463fn is_end(line: &LexedLine<'_>) -> bool {
464    line.kind == LineKind::Statement
465        && line.tokens.len() == 1
466        && line.tokens[0].text.eq_ignore_ascii_case("END")
467}
468
469// ---------------------------------------------------------------------------
470// Statement grammar
471// ---------------------------------------------------------------------------
472
473/// `IN|FROM SUBSYSTEM name [3WLOWVOLTAGE]` at `at`.
474fn parse_in_subsystem(upper: &[String], words: &[&str], at: usize) -> Option<(String, bool)> {
475    if !matches!(upper.get(at)?.as_str(), "IN" | "FROM") {
476        return None;
477    }
478    if upper.get(at + 1)? != "SUBSYSTEM" {
479        return None;
480    }
481    let subsystem = words.get(at + 2)?.trim().to_owned();
482    let mut next = at + 3;
483    let low_voltage_3w = upper.get(next).is_some_and(|word| word == "3WLOWVOLTAGE");
484    if low_voltage_3w {
485        next += 1;
486    }
487    (next == upper.len()).then_some((subsystem, low_voltage_3w))
488}
489
490/// What the tail after an interface name states.
491enum Rating {
492    /// The line ends after the name.
493    Absent,
494    Stated(f64),
495    /// A tail that is not a rating, which rejects the line.
496    Unreadable,
497}
498
499/// An optional `RATING x MW` tail at `at`.
500fn parse_rating(upper: &[String], at: usize) -> Rating {
501    if at >= upper.len() {
502        return Rating::Absent;
503    }
504    if upper[at] != "RATING" {
505        return Rating::Unreadable;
506    }
507    let Some(value) = upper
508        .get(at + 1)
509        .and_then(|word| word.parse::<f64>().ok())
510        .filter(|value| value.is_finite())
511    else {
512        return Rating::Unreadable;
513    };
514    let mut next = at + 2;
515    if upper.get(next).is_some_and(|word| word == "MW") {
516        next += 1;
517    }
518    if next == upper.len() {
519        Rating::Stated(value)
520    } else {
521        Rating::Unreadable
522    }
523}
524
525/// `MONITOR VOLTAGE RANGE scope lo hi` or
526/// `MONITOR VOLTAGE DEVIATION scope down [up]`.
527fn parse_voltage(upper: &[String], words: &[&str]) -> Option<MonitorStatement> {
528    let deviation = match upper.get(2)?.as_str() {
529        "RANGE" => false,
530        "DEVIATION" => true,
531        _ => return None,
532    };
533    let (scope, at) = parse_scope(upper, words, 3)?;
534    let values: Option<Vec<f64>> = upper[at..]
535        .iter()
536        .map(|word| word.parse::<f64>().ok().filter(|value| value.is_finite()))
537        .collect();
538    let values = values?;
539    match (deviation, values.as_slice()) {
540        // A band whose ends run the wrong way round states no range of voltage
541        // magnitudes, so the line stays text rather than reading as a
542        // statement no writer could state again.
543        (false, [vmin, vmax]) => (vmin <= vmax).then_some(MonitorStatement::VoltageRange {
544            scope,
545            vmin: *vmin,
546            vmax: *vmax,
547        }),
548        (true, [down]) => Some(MonitorStatement::VoltageDeviation {
549            scope,
550            down: *down,
551            up: None,
552        }),
553        (true, [down, up]) => Some(MonitorStatement::VoltageDeviation {
554            scope,
555            down: *down,
556            up: Some(*up),
557        }),
558        _ => None,
559    }
560}
561
562/// One scope at `at`, with the index just past it.
563fn parse_scope(upper: &[String], words: &[&str], at: usize) -> Option<(MonitorScope, usize)> {
564    let integer = |offset: usize| upper.get(at + offset)?.parse::<usize>().ok();
565    match upper.get(at)?.as_str() {
566        "ALL" if upper.get(at + 1).is_some_and(|word| word == "BUSES") => {
567            Some((MonitorScope::AllBuses, at + 2))
568        }
569        "SUBSYSTEM" => Some((
570            MonitorScope::Subsystem {
571                name: words.get(at + 1)?.trim().to_owned(),
572            },
573            at + 2,
574        )),
575        "BUS" => Some((
576            MonitorScope::Bus {
577                bus: BusId(integer(1)?),
578            },
579            at + 2,
580        )),
581        "AREA" => Some((MonitorScope::Area { area: integer(1)? }, at + 2)),
582        "ZONE" => Some((MonitorScope::Zone { zone: integer(1)? }, at + 2)),
583        "OWNER" => Some((MonitorScope::Owner { owner: integer(1)? }, at + 2)),
584        "KV" => {
585            let kv = upper.get(at + 1)?.parse::<f64>().ok()?;
586            kv.is_finite().then_some((MonitorScope::Kv { kv }, at + 2))
587        }
588        _ => None,
589    }
590}
591
592/// `i j [ckt]` inside a monitored block.
593fn parse_branch_ref(words: &[&str]) -> Option<BranchRef> {
594    if words.len() > 3 {
595        return None;
596    }
597    let from = words.first()?.parse::<usize>().ok()?;
598    let to = words.get(1)?.parse::<usize>().ok()?;
599    let circuit = words.get(2).map_or("1", |word| word.trim());
600    Some(BranchRef {
601        from: BusId(from),
602        to: BusId(to),
603        circuit: if circuit.is_empty() {
604            "1".to_owned()
605        } else {
606            circuit.to_owned()
607        },
608    })
609}
610
611// ---------------------------------------------------------------------------
612// Writing
613// ---------------------------------------------------------------------------
614
615fn write_scope(scope: &MonitorScope) -> String {
616    match scope {
617        MonitorScope::AllBuses => "ALL BUSES".to_owned(),
618        MonitorScope::Subsystem { name } => format!("SUBSYSTEM '{name}'"),
619        MonitorScope::Bus { bus } => format!("BUS {}", bus.0),
620        MonitorScope::Area { area } => format!("AREA {area}"),
621        MonitorScope::Zone { zone } => format!("ZONE {zone}"),
622        MonitorScope::Owner { owner } => format!("OWNER {owner}"),
623        MonitorScope::Kv { kv } => format!("KV {}", decimal(*kv)),
624    }
625}
626
627fn write_branch_block(
628    head: &str,
629    branches: &[BranchRef],
630    retained: &[RetainedStatement],
631) -> String {
632    use std::fmt::Write as _;
633
634    let mut out = format!("{head}\n");
635    for branch in branches {
636        let _ = writeln!(
637            out,
638            "{:>6} {:>6} {}",
639            branch.from.0,
640            branch.to.0,
641            field(&branch.circuit)
642        );
643    }
644    for statement in retained {
645        out.push_str(&statement.text);
646        out.push('\n');
647    }
648    out.push_str("END\n");
649    out
650}
651
652fn write_statement(statement: &MonitorStatement) -> String {
653    match statement {
654        MonitorStatement::BranchesInSubsystem {
655            subsystem,
656            low_voltage_3w,
657        } => {
658            let tail = if *low_voltage_3w { " 3WLOWVOLTAGE" } else { "" };
659            format!("MONITOR BRANCHES IN SUBSYSTEM '{subsystem}'{tail}\n")
660        }
661        MonitorStatement::TiesFromSubsystem { subsystem } => {
662            format!("MONITOR TIES FROM SUBSYSTEM '{subsystem}'\n")
663        }
664        MonitorStatement::Branches { branches, retained } => {
665            write_branch_block("MONITOR BRANCHES", branches, retained)
666        }
667        MonitorStatement::Interface {
668            name,
669            rating_mw,
670            branches,
671            retained,
672        } => {
673            let head = match rating_mw {
674                Some(rating) => {
675                    format!("MONITOR INTERFACE '{name}' RATING {} MW", decimal(*rating))
676                }
677                None => format!("MONITOR INTERFACE '{name}'"),
678            };
679            write_branch_block(&head, branches, retained)
680        }
681        MonitorStatement::VoltageRange { scope, vmin, vmax } => format!(
682            "MONITOR VOLTAGE RANGE {} {} {}\n",
683            write_scope(scope),
684            decimal(*vmin),
685            decimal(*vmax)
686        ),
687        MonitorStatement::VoltageDeviation { scope, down, up } => {
688            let tail = match up {
689                Some(up) => format!(" {}", decimal(*up)),
690                None => String::new(),
691            };
692            format!(
693                "MONITOR VOLTAGE DEVIATION {} {}{tail}\n",
694                write_scope(scope),
695                decimal(*down)
696            )
697        }
698    }
699}
700
701// ---------------------------------------------------------------------------
702// Resolution against a network
703// ---------------------------------------------------------------------------
704
705/// What a whole monitored element set bound to. Rows are positions in the
706/// tables of the network the set was resolved against.
707#[derive(Debug, Clone, Default, PartialEq)]
708#[non_exhaustive]
709pub struct MonitoredResolution {
710    /// Branches monitored for flow, from every statement that names some.
711    pub branch_rows: BTreeSet<usize>,
712    /// Three winding transformers a `3WLOWVOLTAGE` statement adds.
713    pub transformer_3w_rows: BTreeSet<usize>,
714    /// Branches monitored because they cross a subsystem border.
715    pub tie_rows: BTreeSet<usize>,
716    pub interfaces: Vec<ResolvedInterface>,
717    pub voltage_ranges: Vec<ResolvedVoltageScope>,
718    pub voltage_deviations: Vec<ResolvedVoltageScope>,
719    /// The statements, or the branches inside them, that named nothing.
720    pub unresolved: Vec<UnresolvedMonitor>,
721}
722
723/// One interface and the branches its flow sums over.
724#[derive(Debug, Clone, Default, PartialEq)]
725pub struct ResolvedInterface {
726    pub name: String,
727    pub rating_mw: Option<f64>,
728    /// In statement order; a branch named twice appears twice.
729    pub members: Vec<InterfaceMember>,
730}
731
732/// One branch of an interface, and how the statement stated it against the
733/// stored row.
734///
735/// A `.mon` interface line names its branch in either terminal order, and the
736/// flow of a branch is stated from its stored `from` terminal to its stored
737/// `to` terminal. A member the statement named the other way round therefore
738/// enters the interface sum with its sign flipped, which is what `reversed`
739/// states.
740#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
741pub struct InterfaceMember {
742    /// The row in `net.branches()`.
743    pub row: usize,
744    /// Whether the statement named the stored `to` terminal first.
745    pub reversed: bool,
746}
747
748/// One voltage statement's buses and limits. `high` is absent for a deviation
749/// statement that names one value.
750#[derive(Debug, Clone, Default, PartialEq)]
751pub struct ResolvedVoltageScope {
752    pub bus_rows: BTreeSet<usize>,
753    pub low: f64,
754    pub high: Option<f64>,
755}
756
757/// One statement that named nothing, kept with the reason.
758#[derive(Debug, Clone, PartialEq)]
759pub struct UnresolvedMonitor {
760    pub statement: MonitorStatement,
761    pub reason: UnresolvedMonitorReason,
762}
763
764/// Why a monitor statement named nothing.
765#[derive(Debug, Clone, PartialEq, Eq)]
766#[non_exhaustive]
767pub enum UnresolvedMonitorReason {
768    /// The subsystem set states no subsystem of that name.
769    NoSuchSubsystem,
770    NoSuchBranch {
771        from: BusId,
772        to: BusId,
773        circuit: String,
774    },
775    /// The terminal pair and circuit id name more than one branch.
776    AmbiguousBranch {
777        from: BusId,
778        to: BusId,
779        circuit: String,
780        matches: usize,
781    },
782}
783
784impl MonitoredResolution {
785    /// One `BUILD.MON.STATEMENT_UNRESOLVED` note per unresolved entry.
786    #[must_use]
787    pub fn diagnostics(&self) -> Vec<Diagnostic> {
788        self.unresolved
789            .iter()
790            .map(|entry| {
791                Diagnostic::of(
792                    &codes::BUILD_MON_STATEMENT_UNRESOLVED,
793                    describe(&entry.statement, &entry.reason),
794                )
795            })
796            .collect()
797    }
798}
799
800/// A one line account of a statement that named nothing.
801fn describe(statement: &MonitorStatement, reason: &UnresolvedMonitorReason) -> String {
802    let what = match statement {
803        MonitorStatement::BranchesInSubsystem { subsystem, .. }
804        | MonitorStatement::TiesFromSubsystem { subsystem } => {
805            format!("monitored subsystem '{subsystem}'")
806        }
807        MonitorStatement::Branches { .. } => "monitored branches".to_owned(),
808        MonitorStatement::Interface { name, .. } => format!("monitored interface '{name}'"),
809        MonitorStatement::VoltageRange { .. } => "monitored voltage range".to_owned(),
810        MonitorStatement::VoltageDeviation { .. } => "monitored voltage deviation".to_owned(),
811    };
812    match reason {
813        UnresolvedMonitorReason::NoSuchSubsystem => {
814            format!("{what}: the subsystem set states no such subsystem")
815        }
816        UnresolvedMonitorReason::NoSuchBranch { from, to, circuit } => {
817            format!("{what}: no branch {from} to {to} circuit {circuit}")
818        }
819        UnresolvedMonitorReason::AmbiguousBranch {
820            from,
821            to,
822            circuit,
823            matches,
824        } => format!("{what}: branch {from} to {to} circuit {circuit} names {matches} branches"),
825    }
826}
827
828impl MonitoredSet {
829    /// Bind every statement to the rows of `net`, reading subsystem names from
830    /// `subsystems`.
831    ///
832    /// Resolution reports rather than refuses: a statement naming a subsystem
833    /// or a branch that is not there is kept in
834    /// [`MonitoredResolution::unresolved`] with its reason, and the statements
835    /// that did bind stay listed. A statement kept as text names nothing and
836    /// is not resolved at all.
837    ///
838    /// Service state does not enter: a monitored element is reported on
839    /// whether or not the network states it in service.
840    #[must_use]
841    pub fn resolve(&self, net: &BalancedNetwork, subsystems: &SubsystemSet) -> MonitoredResolution {
842        self.resolve_with(&PsseEquipmentIndex::new(net), subsystems)
843    }
844
845    /// [`MonitoredSet::resolve`] against an index built once, for a caller
846    /// binding several files to one network.
847    ///
848    /// The network is the one the index borrows, so the rows it states always
849    /// index that network's tables.
850    #[must_use]
851    pub fn resolve_with(
852        &self,
853        index: &PsseEquipmentIndex<'_>,
854        subsystems: &SubsystemSet,
855    ) -> MonitoredResolution {
856        let mut out = MonitoredResolution::default();
857        for statement in &self.statements {
858            resolve_statement(statement, index.network(), subsystems, index, &mut out);
859        }
860        out
861    }
862}
863
864fn resolve_statement(
865    statement: &MonitorStatement,
866    net: &BalancedNetwork,
867    subsystems: &SubsystemSet,
868    index: &PsseEquipmentIndex,
869    out: &mut MonitoredResolution,
870) {
871    match statement {
872        MonitorStatement::BranchesInSubsystem {
873            subsystem,
874            low_voltage_3w,
875        } => {
876            let Some(buses) = select(subsystems, subsystem, net) else {
877                unresolved_subsystem(statement, out);
878                return;
879            };
880            for (row, branch) in net.branches().iter().enumerate() {
881                if buses.contains(&branch.from) && buses.contains(&branch.to) {
882                    out.branch_rows.insert(row);
883                }
884            }
885            if *low_voltage_3w {
886                for (row, transformer) in net.transformers_3w().iter().enumerate() {
887                    if buses.contains(&low_voltage_bus(net, index, transformer)) {
888                        out.transformer_3w_rows.insert(row);
889                    }
890                }
891            }
892        }
893        MonitorStatement::TiesFromSubsystem { subsystem } => {
894            let Some(buses) = select(subsystems, subsystem, net) else {
895                unresolved_subsystem(statement, out);
896                return;
897            };
898            for (row, branch) in net.branches().iter().enumerate() {
899                if buses.contains(&branch.from) != buses.contains(&branch.to) {
900                    out.tie_rows.insert(row);
901                }
902            }
903        }
904        MonitorStatement::Branches { branches, .. } => {
905            for branch in branches {
906                match bind_branch(index, branch) {
907                    Ok(row) => {
908                        out.branch_rows.insert(row);
909                    }
910                    Err(reason) => out.unresolved.push(UnresolvedMonitor {
911                        statement: statement.clone(),
912                        reason,
913                    }),
914                }
915            }
916        }
917        MonitorStatement::Interface {
918            name,
919            rating_mw,
920            branches,
921            ..
922        } => {
923            let mut resolved = ResolvedInterface {
924                name: name.clone(),
925                rating_mw: *rating_mw,
926                members: Vec::new(),
927            };
928            for branch in branches {
929                match bind_branch(index, branch) {
930                    Ok(row) => resolved.members.push(member(index, branch, row)),
931                    Err(reason) => out.unresolved.push(UnresolvedMonitor {
932                        statement: statement.clone(),
933                        reason,
934                    }),
935                }
936            }
937            out.interfaces.push(resolved);
938        }
939        MonitorStatement::VoltageRange { scope, vmin, vmax } => {
940            let Some(bus_rows) = scope_rows(scope, net, subsystems, index) else {
941                unresolved_subsystem(statement, out);
942                return;
943            };
944            out.voltage_ranges.push(ResolvedVoltageScope {
945                bus_rows,
946                low: *vmin,
947                high: Some(*vmax),
948            });
949        }
950        MonitorStatement::VoltageDeviation { scope, down, up } => {
951            let Some(bus_rows) = scope_rows(scope, net, subsystems, index) else {
952                unresolved_subsystem(statement, out);
953                return;
954            };
955            out.voltage_deviations.push(ResolvedVoltageScope {
956                bus_rows,
957                low: *down,
958                high: *up,
959            });
960        }
961    }
962}
963
964fn unresolved_subsystem(statement: &MonitorStatement, out: &mut MonitoredResolution) {
965    out.unresolved.push(UnresolvedMonitor {
966        statement: statement.clone(),
967        reason: UnresolvedMonitorReason::NoSuchSubsystem,
968    });
969}
970
971fn select(subsystems: &SubsystemSet, name: &str, net: &BalancedNetwork) -> Option<BTreeSet<BusId>> {
972    Some(subsystems.get(name)?.select_buses(net))
973}
974
975/// One bound branch with its orientation against the stored row. A statement
976/// naming the stored `to` terminal first states the flow the other way round.
977fn member(index: &PsseEquipmentIndex<'_>, branch: &BranchRef, row: usize) -> InterfaceMember {
978    InterfaceMember {
979        row,
980        reversed: index.network().branches()[row].from != branch.from,
981    }
982}
983
984fn bind_branch(
985    index: &PsseEquipmentIndex,
986    branch: &BranchRef,
987) -> std::result::Result<usize, UnresolvedMonitorReason> {
988    let rows = index.branch_rows(branch.from, branch.to, &branch.circuit);
989    match rows.as_slice() {
990        [] => Err(UnresolvedMonitorReason::NoSuchBranch {
991            from: branch.from,
992            to: branch.to,
993            circuit: branch.circuit.clone(),
994        }),
995        [row] => Ok(*row),
996        many => Err(UnresolvedMonitorReason::AmbiguousBranch {
997            from: branch.from,
998            to: branch.to,
999            circuit: branch.circuit.clone(),
1000            matches: many.len(),
1001        }),
1002    }
1003}
1004
1005/// The bus rows one scope names, or `None` when it names a subsystem the
1006/// subsystem set does not state. A scope naming an area, a zone, an owner, a
1007/// bus, or a kV level the network does not hold names no row.
1008fn scope_rows(
1009    scope: &MonitorScope,
1010    net: &BalancedNetwork,
1011    subsystems: &SubsystemSet,
1012    index: &PsseEquipmentIndex,
1013) -> Option<BTreeSet<usize>> {
1014    let rows_of = |buses: &BTreeSet<BusId>| -> BTreeSet<usize> {
1015        buses.iter().filter_map(|bus| index.bus_row(*bus)).collect()
1016    };
1017    let matching = |keep: &dyn Fn(&crate::network::Bus) -> bool| -> BTreeSet<usize> {
1018        net.buses()
1019            .iter()
1020            .enumerate()
1021            .filter(|(_, bus)| keep(bus))
1022            .map(|(row, _)| row)
1023            .collect()
1024    };
1025    Some(match scope {
1026        MonitorScope::AllBuses => (0..net.buses().len()).collect(),
1027        MonitorScope::Subsystem { name } => rows_of(&select(subsystems, name, net)?),
1028        MonitorScope::Bus { bus } => index.bus_row(*bus).into_iter().collect(),
1029        MonitorScope::Area { area } => matching(&|bus| bus.area == *area),
1030        MonitorScope::Zone { zone } => matching(&|bus| bus.zone == *zone),
1031        MonitorScope::Owner { owner } => matching(&|bus| super::sub::owner_of(bus) == *owner),
1032        MonitorScope::Kv { kv } => matching(&|bus| (bus.base_kv - *kv).abs() <= KV_TOLERANCE),
1033    })
1034}