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    /// Every branch with both terminals in a subsystem and an inclusive kV band.
102    BranchesInSubsystemKvRange {
103        subsystem: String,
104        low_voltage_3w: bool,
105        kv_range: (f64, f64),
106    },
107    /// Every tie from a subsystem where the inside terminal is within an inclusive kV band.
108    TiesFromSubsystemKvRange {
109        subsystem: String,
110        kv_range: (f64, f64),
111    },
112    /// A four-value voltage range with distinct pre-contingency limits.
113    VoltageRangeWithPre {
114        scope: MonitorScope,
115        vmin: f64,
116        vmax: f64,
117        pre_min: f64,
118        pre_max: f64,
119    },
120}
121
122/// One branch named by its terminal buses and circuit id.
123#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
124#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
125pub struct BranchRef {
126    pub from: BusId,
127    pub to: BusId,
128    /// An absent circuit id in the source reads as `1`.
129    pub circuit: String,
130}
131
132/// The buses a voltage statement applies to.
133#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
134#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
135#[serde(tag = "kind", rename_all = "snake_case")]
136#[non_exhaustive]
137pub enum MonitorScope {
138    AllBuses,
139    Subsystem {
140        name: String,
141    },
142    Bus {
143        bus: BusId,
144    },
145    Area {
146        area: usize,
147    },
148    Zone {
149        zone: usize,
150    },
151    Owner {
152        owner: usize,
153    },
154    /// Every bus at this base kV.
155    Kv {
156        kv: f64,
157    },
158    /// Subsystem buses filtered to a single base-kV level.
159    SubsystemKv {
160        name: String,
161        kv: f64,
162    },
163}
164
165/// Output of a tolerant monitored element read.
166#[derive(Debug, Clone)]
167#[non_exhaustive]
168pub struct MonitoredParsed {
169    pub set: MonitoredSet,
170    /// The reader's notes as structured records.
171    pub diagnostics: Vec<Diagnostic>,
172}
173
174impl MonitoredParsed {
175    fn note(&mut self, info: &'static crate::diagnostics::DiagnosticInfo, message: String) {
176        note_within_budget(
177            &mut self.diagnostics,
178            info,
179            &codes::READ_MON_NOTES_TRUNCATED,
180            message,
181        );
182    }
183
184    fn unrecognized(&mut self, number: usize, text: &str) {
185        self.note(
186            &codes::READ_MON_STATEMENT_UNRECOGNIZED,
187            format!("line {number}: statement kept as text: {text}"),
188        );
189    }
190}
191
192fn bad(message: String) -> Error {
193    Error::FormatRead {
194        format: FMT,
195        message,
196    }
197}
198
199impl MonitoredSet {
200    /// Read a `.mon` file from UTF-8 text. Keywords are case insensitive.
201    ///
202    /// A statement the grammar does not cover keeps its trimmed line where the
203    /// file stated it, and is reported: on the statement inside an open
204    /// `MONITOR BRANCHES` or `MONITOR INTERFACE` block, and in
205    /// [`MonitoredSet::retained`] outside one. Lines after a file level `END`
206    /// are kept at file level, marked [`RetainedStatement::after_end`], and
207    /// reported once.
208    ///
209    /// # Errors
210    /// [`Error::FormatRead`] when a `MONITOR BRANCHES` or `MONITOR INTERFACE`
211    /// block is still open at end of input. The message names the 1-based
212    /// line.
213    pub fn parse(text: &str) -> Result<MonitoredParsed> {
214        let mut reader = Reader::new();
215        for line in lex(text) {
216            reader.read_line(&line);
217        }
218        reader.finish()
219    }
220
221    /// Write the set as `.mon` text: the header lines as written, the
222    /// statements in order, the statements kept from before the file `END`, a
223    /// final `END`, and then the statements read after that `END`. Every line
224    /// ends with a newline.
225    ///
226    /// A line kept from inside a block is written inside that block, before
227    /// its `END`, so it reads back into the same block.
228    #[must_use]
229    pub fn to_mon(&self) -> String {
230        let mut out = String::new();
231        for line in &self.header {
232            out.push_str(line);
233            out.push('\n');
234        }
235        for statement in &self.statements {
236            out.push_str(&write_statement(statement));
237        }
238        for statement in self.retained.iter().filter(|kept| !kept.after_end) {
239            out.push_str(&statement.text);
240            out.push('\n');
241        }
242        out.push_str("END\n");
243        for statement in self.retained.iter().filter(|kept| kept.after_end) {
244            out.push_str(&statement.text);
245            out.push('\n');
246        }
247        out
248    }
249}
250
251/// A `MONITOR BRANCHES` or `MONITOR INTERFACE` block whose `END` has not been
252/// read yet.
253struct OpenBlock {
254    opened: usize,
255    /// The interface this block belongs to, or `None` for a bare branch list.
256    interface: Option<(String, Option<f64>)>,
257    branches: Vec<BranchRef>,
258    retained: Vec<RetainedStatement>,
259}
260
261/// The reader's state while it walks the lines of one file.
262struct Reader {
263    parsed: MonitoredParsed,
264    block: Option<OpenBlock>,
265    seen_statement: bool,
266    ended: bool,
267    noted_text_after_end: bool,
268    /// The line of the first four-value voltage range, once its remark is recorded.
269    band_order_noted_at: Option<usize>,
270}
271
272impl Reader {
273    fn new() -> Self {
274        Reader {
275            parsed: MonitoredParsed {
276                set: MonitoredSet::default(),
277                diagnostics: Vec::new(),
278            },
279            block: None,
280            seen_statement: false,
281            ended: false,
282            noted_text_after_end: false,
283            band_order_noted_at: None,
284        }
285    }
286
287    fn read_line(&mut self, line: &LexedLine<'_>) {
288        if self.block.is_some() {
289            self.read_block_line(line);
290            return;
291        }
292        if self.ended {
293            self.keep_after_end(line);
294            return;
295        }
296        if !self.take_header(line) {
297            return;
298        }
299        let upper = line.keywords();
300        let words = line.words();
301        self.read_statement(line, &upper, &words);
302    }
303
304    fn finish(self) -> Result<MonitoredParsed> {
305        if let Some(open) = self.block {
306            let what = match open.interface {
307                Some((name, _)) => format!("MONITOR INTERFACE '{name}'"),
308                None => "MONITOR BRANCHES".to_owned(),
309            };
310            return Err(bad(format!("line {}: {what} has no END", open.opened)));
311        }
312        Ok(self.parsed)
313    }
314
315    /// Collect a comment line ahead of the first statement into the header.
316    /// Returns whether the caller should read this line as a statement.
317    fn take_header(&mut self, line: &LexedLine<'_>) -> bool {
318        if self.seen_statement {
319            return line.kind == LineKind::Statement;
320        }
321        match line.kind {
322            LineKind::Blank => false,
323            LineKind::Comment => {
324                self.parsed.set.header.push(line.text.to_owned());
325                false
326            }
327            LineKind::Statement => {
328                self.seen_statement = true;
329                true
330            }
331        }
332    }
333
334    /// Keep a statement line that follows the file level `END`, and report the
335    /// first one. A further bare `END` states nothing and is dropped, because
336    /// files carry one or two of them.
337    ///
338    /// The statement carries `after_end`, and `to_mon` states it after the
339    /// `END` it writes. A line stated there that opens a monitor statement
340    /// would otherwise read back as grammar rather than as text.
341    fn keep_after_end(&mut self, line: &LexedLine<'_>) {
342        if line.kind != LineKind::Statement || is_end(line) {
343            return;
344        }
345        if !self.noted_text_after_end {
346            self.noted_text_after_end = true;
347            self.parsed.note(
348                &codes::READ_MON_TEXT_AFTER_END,
349                format!("line {}: text follows the file END", line.number),
350            );
351        }
352        self.keep_statement(line, true);
353    }
354
355    /// Keep one line as a file level statement. `after_end` marks a line the
356    /// file states after its `END`, which the writer states after the `END` it
357    /// writes.
358    fn keep_statement(&mut self, line: &LexedLine<'_>, after_end: bool) {
359        self.parsed.set.retained.push(RetainedStatement {
360            line: line.number,
361            text: line.trimmed().to_owned(),
362            after_end,
363        });
364    }
365
366    /// Add one branch to the open block, and close the block on its `END`.
367    fn read_block_line(&mut self, line: &LexedLine<'_>) {
368        if line.kind != LineKind::Statement {
369            return;
370        }
371        if is_end(line) {
372            let Some(open) = self.block.take() else {
373                return;
374            };
375            self.parsed.set.statements.push(match open.interface {
376                Some((name, rating_mw)) => MonitorStatement::Interface {
377                    name,
378                    rating_mw,
379                    branches: open.branches,
380                    retained: open.retained,
381                },
382                None => MonitorStatement::Branches {
383                    branches: open.branches,
384                    retained: open.retained,
385                },
386            });
387            return;
388        }
389        let words = line.words();
390        if let Some(branch) = parse_branch_ref(&words) {
391            if let Some(open) = self.block.as_mut() {
392                open.branches.push(branch);
393            }
394            return;
395        }
396        self.parsed.note(
397            &codes::READ_MON_SOURCE_MALFORMED,
398            format!(
399                "line {}: a monitored block line states no branch and was kept as text: {}",
400                line.number,
401                line.trimmed()
402            ),
403        );
404        if let Some(open) = self.block.as_mut() {
405            open.retained.push(RetainedStatement {
406                line: line.number,
407                text: line.trimmed().to_owned(),
408                after_end: false,
409            });
410        }
411    }
412
413    fn read_statement(&mut self, line: &LexedLine<'_>, upper: &[String], words: &[&str]) {
414        if is_end(line) {
415            self.ended = true;
416            return;
417        }
418        if upper[0] == "MONITOR"
419            && let Some(read) = self.read_monitor(line, upper, words)
420        {
421            match read {
422                Read::Statement(statement) => {
423                    if matches!(statement, MonitorStatement::VoltageRangeWithPre { .. }) {
424                        self.note_band_order(line.number);
425                    }
426                    self.parsed.set.statements.push(statement);
427                }
428                Read::OpenedBlock => {}
429            }
430            return;
431        }
432        self.parsed.unrecognized(line.number, line.trimmed());
433        self.keep_statement(line, false);
434    }
435
436    /// Record, once per file, the band order a four-value
437    /// `MONITOR VOLTAGE RANGE` is read in. The PSS/E manual states two values
438    /// (low, then an optional high), and no public source labels the two
439    /// pairs of the four-value form, so the order is an assumption the
440    /// caller should see. See `FORMAT.md`.
441    fn note_band_order(&mut self, number: usize) {
442        if self.band_order_noted_at.is_some() {
443            return;
444        }
445        self.band_order_noted_at = Some(number);
446        self.parsed.note(
447            &codes::READ_MON_VOLTAGE_BAND_ORDER_ASSUMED,
448            format!(
449                "line {number}: MONITOR VOLTAGE RANGE states four values; every four-value range \
450                 in this file is read as the post-contingency low and high, then the \
451                 pre-contingency low and high, an order no public PSS/E or TARA source states"
452            ),
453        );
454    }
455
456    /// One `MONITOR` statement, or `None` when the tail is outside the
457    /// grammar.
458    fn read_monitor(
459        &mut self,
460        line: &LexedLine<'_>,
461        upper: &[String],
462        words: &[&str],
463    ) -> Option<Read> {
464        match upper.get(1)?.as_str() {
465            "BRANCHES" | "LINES" => {
466                if upper.len() == 2 {
467                    self.block = Some(OpenBlock {
468                        opened: line.number,
469                        interface: None,
470                        branches: Vec::new(),
471                        retained: Vec::new(),
472                    });
473                    return Some(Read::OpenedBlock);
474                }
475                let (subsystem, low_voltage_3w, kv_range) = parse_in_subsystem_kv(upper, words, 2)?;
476                Some(Read::Statement(match kv_range {
477                    Some(kv_range) => MonitorStatement::BranchesInSubsystemKvRange {
478                        subsystem,
479                        low_voltage_3w,
480                        kv_range,
481                    },
482                    None => MonitorStatement::BranchesInSubsystem {
483                        subsystem,
484                        low_voltage_3w,
485                    },
486                }))
487            }
488            "TIES" => {
489                let (subsystem, low_voltage_3w, kv_range) = parse_in_subsystem_kv(upper, words, 2)?;
490                (!low_voltage_3w).then_some(Read::Statement(match kv_range {
491                    Some(kv_range) => MonitorStatement::TiesFromSubsystemKvRange {
492                        subsystem,
493                        kv_range,
494                    },
495                    None => MonitorStatement::TiesFromSubsystem { subsystem },
496                }))
497            }
498            "INTERFACE" => {
499                let name = words.get(2)?.trim().to_owned();
500                let rating_mw = match parse_rating(upper, 3) {
501                    Rating::Absent => None,
502                    Rating::Stated(value) => Some(value),
503                    Rating::Unreadable => return None,
504                };
505                self.block = Some(OpenBlock {
506                    opened: line.number,
507                    interface: Some((name, rating_mw)),
508                    branches: Vec::new(),
509                    retained: Vec::new(),
510                });
511                Some(Read::OpenedBlock)
512            }
513            "VOLTAGE" => parse_voltage(upper, words).map(Read::Statement),
514            // Preserve the contingency spelling already merged through #587.
515            "BRANCH" | "LINE" => parse_branch_ref(&words[1..]).map(|branch| {
516                Read::Statement(MonitorStatement::Branches {
517                    branches: vec![branch],
518                    retained: Vec::new(),
519                })
520            }),
521            _ => None,
522        }
523    }
524}
525
526/// What one `MONITOR` line produced.
527enum Read {
528    Statement(MonitorStatement),
529    OpenedBlock,
530}
531
532/// Whether the line is a bare `END`.
533fn is_end(line: &LexedLine<'_>) -> bool {
534    line.kind == LineKind::Statement
535        && line.tokens.len() == 1
536        && line.tokens[0].text.eq_ignore_ascii_case("END")
537}
538
539// ---------------------------------------------------------------------------
540// Statement grammar
541// ---------------------------------------------------------------------------
542
543/// Parsed subsystem monitor head plus its optional base-kV range.
544type SubsystemMonitorSpec = (String, bool, Option<(f64, f64)>);
545
546/// `IN|FROM SUBSYSTEM name [3WLOWVOLTAGE]` at `at`.
547fn parse_in_subsystem_kv(
548    upper: &[String],
549    words: &[&str],
550    at: usize,
551) -> Option<SubsystemMonitorSpec> {
552    let subsystem = words.get(at + 2)?.trim().to_owned();
553    if !matches!(upper.get(at)?.as_str(), "IN" | "FROM") || upper.get(at + 1)? != "SUBSYSTEM" {
554        return None;
555    }
556    let mut next = at + 3;
557    let low_voltage_3w = upper.get(next).is_some_and(|word| word == "3WLOWVOLTAGE");
558    if low_voltage_3w {
559        next += 1;
560    }
561    // PSS/E 35.2 spells the filter `IN KVRANGE lo hi`; TARA drops the `IN`.
562    // Both read the same, and the writer states the bare TARA form.
563    if upper.get(next).is_some_and(|word| word == "IN")
564        && upper.get(next + 1).is_some_and(|word| word == "KVRANGE")
565    {
566        next += 1;
567    }
568    let kv_range = if upper.get(next).is_some_and(|word| word == "KVRANGE") {
569        let lo = upper.get(next + 1)?.parse::<f64>().ok()?;
570        let hi = upper.get(next + 2)?.parse::<f64>().ok()?;
571        if !lo.is_finite() || !hi.is_finite() || lo > hi {
572            return None;
573        }
574        next += 3;
575        Some((lo, hi))
576    } else {
577        None
578    };
579    (next == upper.len()).then_some((subsystem, low_voltage_3w, kv_range))
580}
581
582/// What the tail after an interface name states.
583enum Rating {
584    /// The line ends after the name.
585    Absent,
586    Stated(f64),
587    /// A tail that is not a rating, which rejects the line.
588    Unreadable,
589}
590
591/// An optional `RATING x MW` tail at `at`.
592fn parse_rating(upper: &[String], at: usize) -> Rating {
593    if at >= upper.len() {
594        return Rating::Absent;
595    }
596    if upper[at] != "RATING" {
597        return Rating::Unreadable;
598    }
599    let Some(value) = upper
600        .get(at + 1)
601        .and_then(|word| word.parse::<f64>().ok())
602        .filter(|value| value.is_finite())
603    else {
604        return Rating::Unreadable;
605    };
606    let mut next = at + 2;
607    if upper.get(next).is_some_and(|word| word == "MW") {
608        next += 1;
609    }
610    if next == upper.len() {
611        Rating::Stated(value)
612    } else {
613        Rating::Unreadable
614    }
615}
616
617/// `MONITOR VOLTAGE RANGE scope lo hi` or
618/// `MONITOR VOLTAGE DEVIATION scope down [up]`.
619fn parse_voltage(upper: &[String], words: &[&str]) -> Option<MonitorStatement> {
620    let deviation = match upper.get(2)?.as_str() {
621        "RANGE" => false,
622        "DEVIATION" => true,
623        _ => return None,
624    };
625    let (scope, at) = parse_scope(upper, words, 3)?;
626    let values: Option<Vec<f64>> = upper[at..]
627        .iter()
628        .map(|word| word.parse::<f64>().ok().filter(|value| value.is_finite()))
629        .collect();
630    let values = values?;
631    match (deviation, values.as_slice()) {
632        // A band whose ends run the wrong way round states no range of voltage
633        // magnitudes, so the line stays text rather than reading as a
634        // statement no writer could state again.
635        (false, [vmin, vmax]) => (vmin <= vmax).then_some(MonitorStatement::VoltageRange {
636            scope,
637            vmin: *vmin,
638            vmax: *vmax,
639        }),
640        (false, [vmin, vmax, pre_min, pre_max]) => {
641            (vmin <= vmax && pre_min <= pre_max).then_some(MonitorStatement::VoltageRangeWithPre {
642                scope,
643                vmin: *vmin,
644                vmax: *vmax,
645                pre_min: *pre_min,
646                pre_max: *pre_max,
647            })
648        }
649        (true, [down]) => Some(MonitorStatement::VoltageDeviation {
650            scope,
651            down: *down,
652            up: None,
653        }),
654        (true, [down, up]) => Some(MonitorStatement::VoltageDeviation {
655            scope,
656            down: *down,
657            up: Some(*up),
658        }),
659        _ => None,
660    }
661}
662
663/// One scope at `at`, with the index just past it.
664fn parse_scope(upper: &[String], words: &[&str], at: usize) -> Option<(MonitorScope, usize)> {
665    let integer = |offset: usize| upper.get(at + offset)?.parse::<usize>().ok();
666    match upper.get(at)?.as_str() {
667        "ALL" if upper.get(at + 1).is_some_and(|word| word == "BUSES") => {
668            Some((MonitorScope::AllBuses, at + 2))
669        }
670        "SUBSYSTEM" => {
671            let name = words.get(at + 1)?.trim().to_owned();
672            let mut next = at + 2;
673            let scope = if upper.get(next).is_some_and(|word| word == "KV") {
674                let value = upper.get(next + 1)?.parse::<f64>().ok()?;
675                if !value.is_finite() {
676                    return None;
677                }
678                next += 2;
679                MonitorScope::SubsystemKv { name, kv: value }
680            } else {
681                MonitorScope::Subsystem { name }
682            };
683            Some((scope, next))
684        }
685        "BUS" => Some((
686            MonitorScope::Bus {
687                bus: BusId(integer(1)?),
688            },
689            at + 2,
690        )),
691        "AREA" => Some((MonitorScope::Area { area: integer(1)? }, at + 2)),
692        "ZONE" => Some((MonitorScope::Zone { zone: integer(1)? }, at + 2)),
693        "OWNER" => Some((MonitorScope::Owner { owner: integer(1)? }, at + 2)),
694        "KV" => {
695            let kv = upper.get(at + 1)?.parse::<f64>().ok()?;
696            kv.is_finite().then_some((MonitorScope::Kv { kv }, at + 2))
697        }
698        _ => None,
699    }
700}
701
702/// `i j [ckt]` inside a monitored block, or the contingency file spelling
703/// `[BRANCH | LINE] FROM BUS i TO BUS j [CIRCUIT c]` there, with or without a
704/// leading `MONITOR`, and after `MONITOR` on its own line. A third `TO BUS k` names a three winding
705/// transformer, which a [`BranchRef`] cannot hold, so such a line stays text.
706fn parse_branch_ref(words: &[&str]) -> Option<BranchRef> {
707    // A block line may also repeat the standalone statement whole, `MONITOR`
708    // included.
709    let words = match words {
710        [monitor, noun, ..]
711            if monitor.eq_ignore_ascii_case("MONITOR")
712                && (noun.eq_ignore_ascii_case("BRANCH") || noun.eq_ignore_ascii_case("LINE")) =>
713        {
714            &words[1..]
715        }
716        _ => words,
717    };
718    if words.first()?.parse::<usize>().is_err() {
719        let upper: Vec<String> = words.iter().map(|word| word.to_ascii_uppercase()).collect();
720        let mut at = usize::from(matches!(upper[0].as_str(), "BRANCH" | "LINE"));
721        super::take_keyword(&upper, &mut at, "FROM")?;
722        let from = super::take_bus(&upper, &mut at)?;
723        super::take_keyword(&upper, &mut at, "TO")?;
724        let to = super::take_bus(&upper, &mut at)?;
725        let circuit = super::take_circuit(&upper, words, &mut at)?;
726        return Some(BranchRef { from, to, circuit });
727    }
728    if words.len() > 3 {
729        return None;
730    }
731    let from = words.first()?.parse::<usize>().ok()?;
732    let to = words.get(1)?.parse::<usize>().ok()?;
733    let circuit = words.get(2).map_or("1", |word| word.trim());
734    Some(BranchRef {
735        from: BusId(from),
736        to: BusId(to),
737        circuit: if circuit.is_empty() {
738            "1".to_owned()
739        } else {
740            circuit.to_owned()
741        },
742    })
743}
744
745// ---------------------------------------------------------------------------
746// Writing
747// ---------------------------------------------------------------------------
748
749fn write_scope(scope: &MonitorScope) -> String {
750    match scope {
751        MonitorScope::AllBuses => "ALL BUSES".to_owned(),
752        MonitorScope::Subsystem { name } => format!("SUBSYSTEM '{name}'"),
753        MonitorScope::SubsystemKv { name, kv } => format!("SUBSYSTEM '{name}' KV {}", decimal(*kv)),
754        MonitorScope::Bus { bus } => format!("BUS {}", bus.0),
755        MonitorScope::Area { area } => format!("AREA {area}"),
756        MonitorScope::Zone { zone } => format!("ZONE {zone}"),
757        MonitorScope::Owner { owner } => format!("OWNER {owner}"),
758        MonitorScope::Kv { kv } => format!("KV {}", decimal(*kv)),
759    }
760}
761
762fn write_branch_block(
763    head: &str,
764    branches: &[BranchRef],
765    retained: &[RetainedStatement],
766) -> String {
767    use std::fmt::Write as _;
768
769    let mut out = format!("{head}\n");
770    for branch in branches {
771        let _ = writeln!(
772            out,
773            "{:>6} {:>6} {}",
774            branch.from.0,
775            branch.to.0,
776            field(&branch.circuit)
777        );
778    }
779    for statement in retained {
780        out.push_str(&statement.text);
781        out.push('\n');
782    }
783    out.push_str("END\n");
784    out
785}
786
787fn write_statement(statement: &MonitorStatement) -> String {
788    match statement {
789        MonitorStatement::BranchesInSubsystem {
790            subsystem,
791            low_voltage_3w,
792        } => {
793            let low = if *low_voltage_3w { " 3WLOWVOLTAGE" } else { "" };
794            format!("MONITOR BRANCHES IN SUBSYSTEM '{subsystem}'{low}\n")
795        }
796        MonitorStatement::BranchesInSubsystemKvRange {
797            subsystem,
798            low_voltage_3w,
799            kv_range: (lo, hi),
800        } => {
801            let low = if *low_voltage_3w { " 3WLOWVOLTAGE" } else { "" };
802            format!(
803                "MONITOR BRANCHES IN SUBSYSTEM '{subsystem}'{low} KVRANGE {} {}\n",
804                decimal(*lo),
805                decimal(*hi)
806            )
807        }
808        MonitorStatement::TiesFromSubsystem { subsystem } => {
809            format!("MONITOR TIES FROM SUBSYSTEM '{subsystem}'\n")
810        }
811        MonitorStatement::TiesFromSubsystemKvRange {
812            subsystem,
813            kv_range: (lo, hi),
814        } => {
815            format!(
816                "MONITOR TIES FROM SUBSYSTEM '{subsystem}' KVRANGE {} {}\n",
817                decimal(*lo),
818                decimal(*hi)
819            )
820        }
821        MonitorStatement::Branches { branches, retained } => {
822            write_branch_block("MONITOR BRANCHES", branches, retained)
823        }
824        MonitorStatement::Interface {
825            name,
826            rating_mw,
827            branches,
828            retained,
829        } => {
830            let head = match rating_mw {
831                Some(rating) => {
832                    format!("MONITOR INTERFACE '{name}' RATING {} MW", decimal(*rating))
833                }
834                None => format!("MONITOR INTERFACE '{name}'"),
835            };
836            write_branch_block(&head, branches, retained)
837        }
838        MonitorStatement::VoltageRange { scope, vmin, vmax } => format!(
839            "MONITOR VOLTAGE RANGE {} {} {}\n",
840            write_scope(scope),
841            decimal(*vmin),
842            decimal(*vmax)
843        ),
844        MonitorStatement::VoltageRangeWithPre {
845            scope,
846            vmin,
847            vmax,
848            pre_min,
849            pre_max,
850        } => format!(
851            "MONITOR VOLTAGE RANGE {} {} {} {} {}\n",
852            write_scope(scope),
853            decimal(*vmin),
854            decimal(*vmax),
855            decimal(*pre_min),
856            decimal(*pre_max)
857        ),
858        MonitorStatement::VoltageDeviation { scope, down, up } => {
859            let tail = match up {
860                Some(up) => format!(" {}", decimal(*up)),
861                None => String::new(),
862            };
863            format!(
864                "MONITOR VOLTAGE DEVIATION {} {}{tail}\n",
865                write_scope(scope),
866                decimal(*down)
867            )
868        }
869    }
870}
871
872// ---------------------------------------------------------------------------
873// Resolution against a network
874// ---------------------------------------------------------------------------
875
876/// What a whole monitored element set bound to. Rows are positions in the
877/// tables of the network the set was resolved against.
878#[derive(Debug, Clone, Default, PartialEq)]
879#[non_exhaustive]
880pub struct MonitoredResolution {
881    /// Branches monitored for flow, from every statement that names some.
882    pub branch_rows: BTreeSet<usize>,
883    /// Three winding transformers a `3WLOWVOLTAGE` statement adds.
884    pub transformer_3w_rows: BTreeSet<usize>,
885    /// Branches monitored because they cross a subsystem border.
886    pub tie_rows: BTreeSet<usize>,
887    pub interfaces: Vec<ResolvedInterface>,
888    pub voltage_ranges: Vec<ResolvedVoltageScope>,
889    /// Four-value statements carrying separate pre-contingency voltage bands.
890    pub voltage_ranges_with_pre: Vec<(ResolvedVoltageScope, (f64, f64))>,
891    pub voltage_deviations: Vec<ResolvedVoltageScope>,
892    /// The statements, or the branches inside them, that named nothing.
893    pub unresolved: Vec<UnresolvedMonitor>,
894}
895
896/// One interface and the branches its flow sums over.
897#[derive(Debug, Clone, Default, PartialEq)]
898pub struct ResolvedInterface {
899    pub name: String,
900    pub rating_mw: Option<f64>,
901    /// In statement order; a branch named twice appears twice.
902    pub members: Vec<InterfaceMember>,
903}
904
905/// One branch of an interface, and how the statement stated it against the
906/// stored row.
907///
908/// A `.mon` interface line names its branch in either terminal order, and the
909/// flow of a branch is stated from its stored `from` terminal to its stored
910/// `to` terminal. A member the statement named the other way round therefore
911/// enters the interface sum with its sign flipped, which is what `reversed`
912/// states.
913#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
914pub struct InterfaceMember {
915    /// The row in `net.branches()`.
916    pub row: usize,
917    /// Whether the statement named the stored `to` terminal first.
918    pub reversed: bool,
919}
920
921/// One voltage statement's buses and limits. `high` is absent for a deviation
922/// statement that names one value.
923#[derive(Debug, Clone, Default, PartialEq)]
924pub struct ResolvedVoltageScope {
925    pub bus_rows: BTreeSet<usize>,
926    pub low: f64,
927    pub high: Option<f64>,
928}
929
930/// One statement that named nothing, kept with the reason.
931#[derive(Debug, Clone, PartialEq)]
932pub struct UnresolvedMonitor {
933    pub statement: MonitorStatement,
934    pub reason: UnresolvedMonitorReason,
935}
936
937/// Why a monitor statement named nothing.
938#[derive(Debug, Clone, PartialEq, Eq)]
939#[non_exhaustive]
940pub enum UnresolvedMonitorReason {
941    /// The subsystem set states no subsystem of that name.
942    NoSuchSubsystem,
943    NoSuchBranch {
944        from: BusId,
945        to: BusId,
946        circuit: String,
947    },
948    /// The terminal pair and circuit id name more than one branch.
949    AmbiguousBranch {
950        from: BusId,
951        to: BusId,
952        circuit: String,
953        matches: usize,
954    },
955}
956
957impl MonitoredResolution {
958    /// One `BUILD.MON.STATEMENT_UNRESOLVED` note per unresolved entry.
959    #[must_use]
960    pub fn diagnostics(&self) -> Vec<Diagnostic> {
961        self.unresolved
962            .iter()
963            .map(|entry| {
964                Diagnostic::of(
965                    &codes::BUILD_MON_STATEMENT_UNRESOLVED,
966                    describe(&entry.statement, &entry.reason),
967                )
968            })
969            .collect()
970    }
971}
972
973/// A one line account of a statement that named nothing.
974fn describe(statement: &MonitorStatement, reason: &UnresolvedMonitorReason) -> String {
975    let what = match statement {
976        MonitorStatement::BranchesInSubsystem { subsystem, .. }
977        | MonitorStatement::BranchesInSubsystemKvRange { subsystem, .. }
978        | MonitorStatement::TiesFromSubsystem { subsystem }
979        | MonitorStatement::TiesFromSubsystemKvRange { subsystem, .. } => {
980            format!("monitored subsystem '{subsystem}'")
981        }
982        MonitorStatement::Branches { .. } => "monitored branches".to_owned(),
983        MonitorStatement::Interface { name, .. } => format!("monitored interface '{name}'"),
984        MonitorStatement::VoltageRange { .. } | MonitorStatement::VoltageRangeWithPre { .. } => {
985            "monitored voltage range".to_owned()
986        }
987        MonitorStatement::VoltageDeviation { .. } => "monitored voltage deviation".to_owned(),
988    };
989    match reason {
990        UnresolvedMonitorReason::NoSuchSubsystem => {
991            format!("{what}: the subsystem set states no such subsystem")
992        }
993        UnresolvedMonitorReason::NoSuchBranch { from, to, circuit } => {
994            format!("{what}: no branch {from} to {to} circuit {circuit}")
995        }
996        UnresolvedMonitorReason::AmbiguousBranch {
997            from,
998            to,
999            circuit,
1000            matches,
1001        } => format!("{what}: branch {from} to {to} circuit {circuit} names {matches} branches"),
1002    }
1003}
1004
1005impl MonitoredSet {
1006    /// Bind every statement to the rows of `net`, reading subsystem names from
1007    /// `subsystems`.
1008    ///
1009    /// Resolution reports rather than refuses: a statement naming a subsystem
1010    /// or a branch that is not there is kept in
1011    /// [`MonitoredResolution::unresolved`] with its reason, and the statements
1012    /// that did bind stay listed. A statement kept as text names nothing and
1013    /// is not resolved at all.
1014    ///
1015    /// Service state does not enter: a monitored element is reported on
1016    /// whether or not the network states it in service.
1017    #[must_use]
1018    pub fn resolve(&self, net: &BalancedNetwork, subsystems: &SubsystemSet) -> MonitoredResolution {
1019        self.resolve_with(&PsseEquipmentIndex::new(net), subsystems)
1020    }
1021
1022    /// [`MonitoredSet::resolve`] against an index built once, for a caller
1023    /// binding several files to one network.
1024    ///
1025    /// The network is the one the index borrows, so the rows it states always
1026    /// index that network's tables.
1027    #[must_use]
1028    pub fn resolve_with(
1029        &self,
1030        index: &PsseEquipmentIndex<'_>,
1031        subsystems: &SubsystemSet,
1032    ) -> MonitoredResolution {
1033        let mut out = MonitoredResolution::default();
1034        for statement in &self.statements {
1035            resolve_statement(statement, index.network(), subsystems, index, &mut out);
1036        }
1037        out
1038    }
1039}
1040
1041#[allow(clippy::too_many_lines)]
1042fn resolve_statement(
1043    statement: &MonitorStatement,
1044    net: &BalancedNetwork,
1045    subsystems: &SubsystemSet,
1046    index: &PsseEquipmentIndex,
1047    out: &mut MonitoredResolution,
1048) {
1049    match statement {
1050        MonitorStatement::BranchesInSubsystem {
1051            subsystem,
1052            low_voltage_3w,
1053        } => resolve_branches_in_subsystem(
1054            statement,
1055            subsystem,
1056            *low_voltage_3w,
1057            None,
1058            net,
1059            subsystems,
1060            index,
1061            out,
1062        ),
1063        MonitorStatement::BranchesInSubsystemKvRange {
1064            subsystem,
1065            low_voltage_3w,
1066            kv_range,
1067        } => resolve_branches_in_subsystem(
1068            statement,
1069            subsystem,
1070            *low_voltage_3w,
1071            Some(*kv_range),
1072            net,
1073            subsystems,
1074            index,
1075            out,
1076        ),
1077        MonitorStatement::TiesFromSubsystem { subsystem } => {
1078            resolve_ties_from_subsystem(statement, subsystem, None, net, subsystems, index, out);
1079        }
1080        MonitorStatement::TiesFromSubsystemKvRange {
1081            subsystem,
1082            kv_range,
1083        } => {
1084            resolve_ties_from_subsystem(
1085                statement,
1086                subsystem,
1087                Some(*kv_range),
1088                net,
1089                subsystems,
1090                index,
1091                out,
1092            );
1093        }
1094        MonitorStatement::Branches { branches, .. } => {
1095            for branch in branches {
1096                match bind_branch(index, branch) {
1097                    Ok(row) => {
1098                        out.branch_rows.insert(row);
1099                    }
1100                    Err(reason) => out.unresolved.push(UnresolvedMonitor {
1101                        statement: statement.clone(),
1102                        reason,
1103                    }),
1104                }
1105            }
1106        }
1107        MonitorStatement::Interface {
1108            name,
1109            rating_mw,
1110            branches,
1111            ..
1112        } => {
1113            let mut resolved = ResolvedInterface {
1114                name: name.clone(),
1115                rating_mw: *rating_mw,
1116                members: Vec::new(),
1117            };
1118            for branch in branches {
1119                match bind_branch(index, branch) {
1120                    Ok(row) => resolved.members.push(member(index, branch, row)),
1121                    Err(reason) => out.unresolved.push(UnresolvedMonitor {
1122                        statement: statement.clone(),
1123                        reason,
1124                    }),
1125                }
1126            }
1127            out.interfaces.push(resolved);
1128        }
1129        MonitorStatement::VoltageRange { scope, vmin, vmax } => {
1130            let Some(bus_rows) = scope_rows(scope, net, subsystems, index) else {
1131                unresolved_subsystem(statement, out);
1132                return;
1133            };
1134            out.voltage_ranges.push(ResolvedVoltageScope {
1135                bus_rows,
1136                low: *vmin,
1137                high: Some(*vmax),
1138            });
1139        }
1140        MonitorStatement::VoltageRangeWithPre {
1141            scope,
1142            vmin,
1143            vmax,
1144            pre_min,
1145            pre_max,
1146        } => {
1147            let Some(bus_rows) = scope_rows(scope, net, subsystems, index) else {
1148                unresolved_subsystem(statement, out);
1149                return;
1150            };
1151            out.voltage_ranges_with_pre.push((
1152                ResolvedVoltageScope {
1153                    bus_rows,
1154                    low: *vmin,
1155                    high: Some(*vmax),
1156                },
1157                (*pre_min, *pre_max),
1158            ));
1159        }
1160        MonitorStatement::VoltageDeviation { scope, down, up } => {
1161            let Some(bus_rows) = scope_rows(scope, net, subsystems, index) else {
1162                unresolved_subsystem(statement, out);
1163                return;
1164            };
1165            out.voltage_deviations.push(ResolvedVoltageScope {
1166                bus_rows,
1167                low: *down,
1168                high: *up,
1169            });
1170        }
1171    }
1172}
1173
1174#[allow(clippy::too_many_arguments)]
1175fn resolve_branches_in_subsystem(
1176    statement: &MonitorStatement,
1177    subsystem: &str,
1178    low_voltage_3w: bool,
1179    kv_range: Option<(f64, f64)>,
1180    net: &BalancedNetwork,
1181    subsystems: &SubsystemSet,
1182    index: &PsseEquipmentIndex,
1183    out: &mut MonitoredResolution,
1184) {
1185    let Some(buses) = select(subsystems, subsystem, net) else {
1186        unresolved_subsystem(statement, out);
1187        return;
1188    };
1189    for (row, branch) in net.branches().iter().enumerate() {
1190        if buses.contains(&branch.from)
1191            && buses.contains(&branch.to)
1192            && kv_range.is_none_or(|(lo, hi)| {
1193                bus_in_kv_range(index, branch.from, lo, hi)
1194                    && bus_in_kv_range(index, branch.to, lo, hi)
1195            })
1196        {
1197            out.branch_rows.insert(row);
1198        }
1199    }
1200    if low_voltage_3w {
1201        for (row, transformer) in net.transformers_3w().iter().enumerate() {
1202            let low_bus = low_voltage_bus(net, index, transformer);
1203            if buses.contains(&low_bus)
1204                && kv_range.is_none_or(|(lo, hi)| bus_in_kv_range(index, low_bus, lo, hi))
1205            {
1206                out.transformer_3w_rows.insert(row);
1207            }
1208        }
1209    }
1210}
1211
1212fn resolve_ties_from_subsystem(
1213    statement: &MonitorStatement,
1214    subsystem: &str,
1215    kv_range: Option<(f64, f64)>,
1216    net: &BalancedNetwork,
1217    subsystems: &SubsystemSet,
1218    index: &PsseEquipmentIndex,
1219    out: &mut MonitoredResolution,
1220) {
1221    let Some(buses) = select(subsystems, subsystem, net) else {
1222        unresolved_subsystem(statement, out);
1223        return;
1224    };
1225    for (row, branch) in net.branches().iter().enumerate() {
1226        let from_inside = buses.contains(&branch.from);
1227        let to_inside = buses.contains(&branch.to);
1228        let in_scope = match (from_inside, to_inside, kv_range) {
1229            (true, false, Some((lo, hi))) => bus_in_kv_range(index, branch.from, lo, hi),
1230            (false, true, Some((lo, hi))) => bus_in_kv_range(index, branch.to, lo, hi),
1231            (true, false, None) | (false, true, None) => true,
1232            _ => false,
1233        };
1234        if from_inside != to_inside && in_scope {
1235            out.tie_rows.insert(row);
1236        }
1237    }
1238}
1239
1240fn unresolved_subsystem(statement: &MonitorStatement, out: &mut MonitoredResolution) {
1241    out.unresolved.push(UnresolvedMonitor {
1242        statement: statement.clone(),
1243        reason: UnresolvedMonitorReason::NoSuchSubsystem,
1244    });
1245}
1246
1247fn select(subsystems: &SubsystemSet, name: &str, net: &BalancedNetwork) -> Option<BTreeSet<BusId>> {
1248    Some(subsystems.get(name)?.select_buses(net))
1249}
1250
1251fn bus_in_kv_range(index: &PsseEquipmentIndex<'_>, bus: BusId, lo: f64, hi: f64) -> bool {
1252    index.bus_row(bus).is_some_and(|row| {
1253        let kv = index.network().buses()[row].base_kv;
1254        kv >= lo - KV_TOLERANCE && kv <= hi + KV_TOLERANCE
1255    })
1256}
1257
1258/// One bound branch with its orientation against the stored row. A statement
1259/// naming the stored `to` terminal first states the flow the other way round.
1260fn member(index: &PsseEquipmentIndex<'_>, branch: &BranchRef, row: usize) -> InterfaceMember {
1261    InterfaceMember {
1262        row,
1263        reversed: index.network().branches()[row].from != branch.from,
1264    }
1265}
1266
1267fn bind_branch(
1268    index: &PsseEquipmentIndex,
1269    branch: &BranchRef,
1270) -> std::result::Result<usize, UnresolvedMonitorReason> {
1271    let rows = index.branch_rows(branch.from, branch.to, &branch.circuit);
1272    match rows.as_slice() {
1273        [] => Err(UnresolvedMonitorReason::NoSuchBranch {
1274            from: branch.from,
1275            to: branch.to,
1276            circuit: branch.circuit.clone(),
1277        }),
1278        [row] => Ok(*row),
1279        many => Err(UnresolvedMonitorReason::AmbiguousBranch {
1280            from: branch.from,
1281            to: branch.to,
1282            circuit: branch.circuit.clone(),
1283            matches: many.len(),
1284        }),
1285    }
1286}
1287
1288/// The bus rows one scope names, or `None` when it names a subsystem the
1289/// subsystem set does not state. A scope naming an area, a zone, an owner, a
1290/// bus, or a kV level the network does not hold names no row.
1291fn scope_rows(
1292    scope: &MonitorScope,
1293    net: &BalancedNetwork,
1294    subsystems: &SubsystemSet,
1295    index: &PsseEquipmentIndex,
1296) -> Option<BTreeSet<usize>> {
1297    let rows_of = |buses: &BTreeSet<BusId>| -> BTreeSet<usize> {
1298        buses.iter().filter_map(|bus| index.bus_row(*bus)).collect()
1299    };
1300    let matching = |keep: &dyn Fn(&crate::network::Bus) -> bool| -> BTreeSet<usize> {
1301        net.buses()
1302            .iter()
1303            .enumerate()
1304            .filter(|(_, bus)| keep(bus))
1305            .map(|(row, _)| row)
1306            .collect()
1307    };
1308    Some(match scope {
1309        MonitorScope::AllBuses => (0..net.buses().len()).collect(),
1310        MonitorScope::Subsystem { name } => rows_of(&select(subsystems, name, net)?),
1311        MonitorScope::SubsystemKv { name, kv } => select(subsystems, name, net)?
1312            .into_iter()
1313            .filter(|bus| bus_in_kv_range(index, *bus, *kv, *kv))
1314            .filter_map(|bus| index.bus_row(bus))
1315            .collect(),
1316        MonitorScope::Bus { bus } => index.bus_row(*bus).into_iter().collect(),
1317        MonitorScope::Area { area } => matching(&|bus| bus.area == *area),
1318        MonitorScope::Zone { zone } => matching(&|bus| bus.zone == *zone),
1319        MonitorScope::Owner { owner } => matching(&|bus| super::sub::owner_of(bus) == *owner),
1320        MonitorScope::Kv { kv } => matching(&|bus| (bus.base_kv - *kv).abs() <= KV_TOLERANCE),
1321    })
1322}